handlePortExpand method

Future<SchematicLayoutResult?> handlePortExpand(
  1. String nodeId,
  2. String portId
)

Handle incremental port expansion.

Reveals only the internal edges connected to portId on node nodeId, plus the submodules at the other end of those edges. This is the Dart-first path only (port expansion is not supported in JS-first mode).

Implementation

Future<SchematicLayoutResult?> handlePortExpand(
  String nodeId,
  String portId,
) async {
  if (isToggling) {
    return null;
  }
  final engine = _layoutEngine;
  if (engine == null || schematicAdapter == null) {
    return null;
  }

  // Show the spinner only when a network fetch is actually needed.
  if (needsConnectivity(nodeId)) {
    setState(() {
      isToggling = true;
      togglingLabel = 'Expanding...';
      pendingToggleNodeId = nodeId;
    });
    // Yield to let the spinner frame paint before the heavy fetch.
    final completer = Completer<void>();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      completer.complete();
    });
    await completer.future;
    SchedulerBinding.instance.ensureVisualUpdate();
    // Fetch + rebuild adapter.
    await ensureConnectivity(nodeId);
  } else {
    // Already loaded — gate re-entry without triggering a repaint or
    // showing the spinner overlay.  Direct field mutation is safe here
    // because Dart is single-threaded: the gate is set synchronously
    // before the first await below.
    isToggling = true;
    pendingToggleNodeId = nodeId;
    await ensureConnectivity(nodeId); // fast no-op
  }

  final preExpandSnapshot = schematicAdapter!.schematic.expansionSnapshot();
  try {
    final expanded = schematicAdapter!.expandPort(nodeId, portId);
    if (!expanded) {
      if (mounted) {
        setState(() {
          isToggling = false;
          pendingToggleNodeId = null;
        });
      }
      return null;
    }

    final elkGraph = schematicAdapter!.schematic.toJsGraph();
    final newLayout = await engine.computeLayoutFromElkGraph(
      elkGraph,
      sessionId: _sessionId,
    );

    if (newLayout.hasError || !mounted) {
      schematicAdapter!.schematic.restoreExpansionSnapshot(preExpandSnapshot);
      if (mounted) {
        setState(() {
          isToggling = false;
          pendingToggleNodeId = null;
        });
      }
      return null;
    }

    setState(() {
      layout = newLayout;
      isToggling = false;
      pendingToggleNodeId = null;
      // Focus on the specific port that was clicked so the canvas
      // pans to keep it centred at the current zoom level.
      focusPortId = portId;
      recentlyToggledNodeId = null; // port focus overrides node fit
    });

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        setState(() {
          focusPortId = null;
        });
      }
    });

    return newLayout;
  } on Exception {
    schematicAdapter!.schematic.restoreExpansionSnapshot(preExpandSnapshot);
    if (mounted) {
      setState(() {
        isToggling = false;
        pendingToggleNodeId = null;
      });
    }
    return null;
  }
}