diff --git a/lib/src/widgets/edge_painter.dart b/lib/src/widgets/edge_painter.dart index c3dc4c3..06cbf3e 100644 --- a/lib/src/widgets/edge_painter.dart +++ b/lib/src/widgets/edge_painter.dart @@ -22,12 +22,22 @@ class EdgeSegment { final EdgeSide fromSide; final EdgeSide toSide; final EdgeAccent accent; + + /// When false, the bezier endpoint is NOT shortened by + /// `portRadius` on the corresponding side. Used by the + /// draft-drag segment so the line actually reaches the + /// cursor instead of stopping a port-radius short of it + /// (the cursor is a mouse position, not a port socket). + final bool shortenFrom; + final bool shortenTo; const EdgeSegment({ required this.from, required this.to, required this.fromSide, required this.toSide, this.accent = EdgeAccent.normal, + this.shortenFrom = true, + this.shortenTo = true, }); } @@ -75,15 +85,14 @@ class EdgePainter extends CustomPainter { // a left-side port has its outer perimeter to the left // of its centre, so we move the endpoint LEFT by // radius; right-side port goes the other way. - final shortenedFrom = Offset( - seg.from.dx + - (seg.fromSide == EdgeSide.right ? portRadius : -portRadius), - seg.from.dy, - ); - final shortenedTo = Offset( - seg.to.dx + (seg.toSide == EdgeSide.right ? portRadius : -portRadius), - seg.to.dy, - ); + final fromShift = seg.shortenFrom + ? (seg.fromSide == EdgeSide.right ? portRadius : -portRadius) + : 0.0; + final toShift = seg.shortenTo + ? (seg.toSide == EdgeSide.right ? portRadius : -portRadius) + : 0.0; + final shortenedFrom = Offset(seg.from.dx + fromShift, seg.from.dy); + final shortenedTo = Offset(seg.to.dx + toShift, seg.to.dy); canvas.drawPath(_bezier(shortenedFrom, shortenedTo), paint); } } diff --git a/lib/src/widgets/flow_canvas.dart b/lib/src/widgets/flow_canvas.dart index ccac4e3..719c288 100644 --- a/lib/src/widgets/flow_canvas.dart +++ b/lib/src/widgets/flow_canvas.dart @@ -74,6 +74,12 @@ class _FlowCanvasState extends State { // paints a draft edge from the source port to the cursor // and accepts a drop on any input port. _ConnectionDraft? _draft; + // Currently-hovered port id. Drives the hover halo so the + // operator gets a clear "this port is interactive" + // affordance before they commit to dragging or clicking. + // String key = same shape used by _connectedPorts so we + // can pass a single hovered-key into the port widget. + String? _hoveredPort; @override void initState() { @@ -201,6 +207,13 @@ class _FlowCanvasState extends State { fromSide: EdgeSide.right, toSide: EdgeSide.left, accent: EdgeAccent.draftDrag, + // The draft target IS the + // cursor — not a port socket. + // Don't shorten on that end or + // the line stops short of + // where the operator's mouse + // actually is. + shortenTo: false, ), ], baseColor: theme.colorScheme.primary, @@ -522,6 +535,7 @@ class _FlowCanvasState extends State { final p = _outputPortPosition(step.id, layout); final key = '${step.id}:__out__'; yield _portDot( + portKey: key, center: p, isSource: true, connected: connectedPorts.contains(key), @@ -542,6 +556,7 @@ class _FlowCanvasState extends State { final input = graph.inputs[fieldName]!; final key = 'inputs:$fieldName'; yield _portDot( + portKey: key, center: p, isSource: true, connected: connectedPorts.contains(key), @@ -564,6 +579,7 @@ class _FlowCanvasState extends State { final value = step.with_[field]?.toString() ?? ''; final wired = _isWiredExpression(value); yield _portDot( + portKey: '${step.id}:$field', center: p, isSource: false, connected: wired, @@ -582,6 +598,7 @@ class _FlowCanvasState extends State { final expr = graph.outputs[field] ?? ''; final wired = _isWiredExpression(expr); yield _portDot( + portKey: 'outputs:$field', center: p, isSource: false, connected: wired, @@ -717,6 +734,7 @@ class _FlowCanvasState extends State { } Widget _portDot({ + required String portKey, required Offset center, required bool isSource, required bool connected, @@ -728,11 +746,16 @@ class _FlowCanvasState extends State { final dragging = _draft != null; final isInputDuringDrag = dragging && !isSource; final isClosest = isInputDuringDrag && _isClosestDropTarget(center); - // Size logic: drop-target halo when a drag is hovering - // → 18 px; otherwise the standard 12 px port that - // matches NodeGeometry.portDotSize. One single dot per - // port — no more separate inline/canvas dots. - final size = isClosest ? 18.0 : NodeGeometry.portDotSize; + final isHovered = _hoveredPort == portKey; + // Size scales with focus level: + // - drop-target halo (drag is over this port) → 18 px + // - hover (mouse-over without dragging) → 15 px + // - resting → 12 px + // The hover bump is the new "this port is interactive" + // affordance Stefan asked for. + final size = isClosest + ? 18.0 + : (isHovered ? 15.0 : NodeGeometry.portDotSize); // Fill rule: filled when this port participates in an // edge, OR it's the closest drop target mid-drag. Plain // outlined circle when neither — the operator sees at @@ -745,6 +768,12 @@ class _FlowCanvasState extends State { height: size, child: MouseRegion( cursor: isSource ? SystemMouseCursors.grab : SystemMouseCursors.cell, + onEnter: (_) => setState(() => _hoveredPort = portKey), + onExit: (_) { + if (_hoveredPort == portKey) { + setState(() => _hoveredPort = null); + } + }, child: GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: !isSource @@ -785,13 +814,15 @@ class _FlowCanvasState extends State { color: filled ? accent : theme.colorScheme.surface, border: Border.all( color: accent, - width: isClosest ? 2.5 : 1.8, + width: isClosest ? 2.5 : (isHovered ? 2.2 : 1.8), ), - boxShadow: isClosest + boxShadow: (isClosest || isHovered) ? [ BoxShadow( - color: accent.withValues(alpha: 0.5), - blurRadius: 10, + color: accent.withValues( + alpha: isClosest ? 0.55 : 0.35, + ), + blurRadius: isClosest ? 10 : 6, ), ] : null, diff --git a/pubspec.yaml b/pubspec.yaml index 37a4c7d..5a5a5e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fai_studio_flow_editor description: Swappable inline YAML editor for F∆I Studio flows. -version: 0.5.2 +version: 0.5.3 publish_to: 'none' repository: https://git.flemming.ai/fai/studio-flow-editor