feat(editor): highlight drop targets during connection drag

While the operator is dragging from an output port, every
input port now lights up with a tint so they can see at a
glance where the connection can land. The closest port
within the snap distance gets a larger halo + brighter
border so the operator knows which port will accept the
drop if they release now.

Visual states:
 - input port, no drag:        muted background
 - input port, drag in flight: primary @ 35% alpha
 - input port, closest target: primary fill + 2.5 px border
                                + soft halo glow

The closest-target computation runs once per port per
frame during a drag — O(n) over visible input ports.
Negligible at flow sizes operators care about; revisit if
flows exceed a few dozen steps.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-01 01:59:33 +02:00
parent 307eaccc3e
commit fb8730903c

View file

@ -483,7 +483,15 @@ class _FlowCanvasState extends State<FlowCanvas> {
void Function(Offset cursor)? onDropTarget, void Function(Offset cursor)? onDropTarget,
}) { }) {
final theme = Theme.of(context); final theme = Theme.of(context);
const size = 16.0; // During a connection drag, highlight every input port
// so the operator sees the landing options; the closest
// one gets a larger halo so they know which port the
// drop will snap to if they release now.
final dragging = _draft != null;
final isInputDuringDrag = dragging && !isSource;
final isClosest =
isInputDuringDrag && _isClosestDropTarget(center);
final size = isClosest ? 22.0 : 16.0;
return Positioned( return Positioned(
left: center.dx - size / 2, left: center.dx - size / 2,
top: center.dy - size / 2, top: center.dy - size / 2,
@ -517,10 +525,27 @@ class _FlowCanvasState extends State<FlowCanvas> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: isSource color: isClosest
? theme.colorScheme.primary ? theme.colorScheme.primary
: isSource
? theme.colorScheme.primary
: isInputDuringDrag
? theme.colorScheme.primary
.withValues(alpha: 0.35)
: theme.colorScheme.surfaceContainerHighest, : theme.colorScheme.surfaceContainerHighest,
border: Border.all(color: theme.colorScheme.primary, width: 1.5), border: Border.all(
color: theme.colorScheme.primary,
width: isClosest ? 2.5 : 1.5,
),
boxShadow: isClosest
? [
BoxShadow(
color: theme.colorScheme.primary
.withValues(alpha: 0.5),
blurRadius: 10,
),
]
: null,
), ),
), ),
), ),
@ -528,6 +553,43 @@ class _FlowCanvasState extends State<FlowCanvas> {
); );
} }
/// True if this input port is the nearest valid drop
/// target to the current draft cursor (within snap
/// distance). Used to paint the highlight halo so the
/// operator sees which port will accept the connection.
bool _isClosestDropTarget(Offset portCenter) {
final draft = _draft;
if (draft == null) return false;
final graph = widget.controller.graph;
final layout = widget.controller.layout;
final outputsX = _outputsX(graph, layout);
const maxDist = 32.0;
double bestDist = double.infinity;
Offset? best;
for (final step in graph.steps) {
final keys = step.with_.keys.toList();
for (var i = 0; i < keys.length; i++) {
final p = _inputPortPosition(step.id, i, layout, outputsX);
final d = (draft.cursor - p).distance;
if (d < bestDist && d <= maxDist) {
bestDist = d;
best = p;
}
}
}
final outs = graph.outputs.keys.toList();
for (var i = 0; i < outs.length; i++) {
final p = _inputPortPosition('__outputs__', i, layout, outputsX);
final d = (draft.cursor - p).distance;
if (d < bestDist && d <= maxDist) {
bestDist = d;
best = p;
}
}
if (best == null) return false;
return (best - portCenter).distance < 0.5;
}
void Function(Offset) _dropTargetFor( void Function(Offset) _dropTargetFor(
String targetId, String targetId,
String targetField, String targetField,