handleNodeToggle method

Future<SchematicLayoutResult?> handleNodeToggle(
  1. String nodeId
)

Handle node toggle (expand/collapse).

Uses either Dart-first or JS-first toggle based on useDartFirstParsing.

Implementation

Future<SchematicLayoutResult?> handleNodeToggle(String nodeId) async {
  if (isToggling) {
    return null;
  }
  final engine = _layoutEngine;
  if (engine == null) {
    return null;
  }

  // Determine label before the toggle mutates state.
  final wasExpanded = schematicAdapter?.isExpanded(nodeId) ?? false;

  setState(() {
    isToggling = true;
    togglingLabel = wasExpanded ? 'Collapsing...' : 'Expanding...';
    pendingToggleNodeId = nodeId;
  });

  // Wait until the frame is painted and spinner animation starts
  // before beginning heavy processing
  final completer = Completer<void>();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    completer.complete();
  });
  await completer.future;

  // Request visual updates during processing to keep spinner animating
  SchedulerBinding.instance.ensureVisualUpdate();

  // Ensure connectivity is available for this module before toggling.
  await ensureConnectivity(nodeId);

  try {
    SchematicLayoutResult newLayout;

    if (schematicAdapter == null) {
      if (!mounted) {
        return null;
      }
      setState(() {
        isToggling = false;
        pendingToggleNodeId = null;
      });
      return null;
    }

    // Toggle in Dart, then re-serialize for layout

    // Snapshot expansion state before mutating, so we can restore
    // exactly if ELK rejects the resulting graph (e.g. partial → full
    // fails, but a blind re-toggle would go full → collapsed instead).
    final preToggleSnapshot = schematicAdapter!.schematic.expansionSnapshot();

    // Toggle in the Dart-owned graph
    final toggled = schematicAdapter!.toggleNode(nodeId);
    if (!toggled) {
      if (!mounted) {
        return null;
      }
      setState(() {
        isToggling = false;
        pendingToggleNodeId = null;
      });
      return null;
    }

    // Re-serialize and compute layout
    final elkGraph = schematicAdapter!.schematic.toJsGraph();

    newLayout = await engine.computeLayoutFromElkGraph(
      elkGraph,
      sessionId: _sessionId,
    );

    if (newLayout.hasError) {
      if (!mounted) {
        return null;
      }
      // Restore to pre-toggle state so the adapter stays consistent
      // with the visible layout (which was not updated due to the error).
      schematicAdapter!.schematic.restoreExpansionSnapshot(preToggleSnapshot);
      setState(() {
        isToggling = false;
        pendingToggleNodeId = null;
      });
      return null;
    }

    if (!mounted) {
      return null;
    }

    setState(() {
      layout = newLayout;
      isToggling = false;
      pendingToggleNodeId = null;
      recentlyToggledNodeId = nodeId;
    });

    // Clear the recently toggled node after the next frame
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        setState(() {
          recentlyToggledNodeId = null;
        });
      }
    });

    return newLayout;
  } on Exception {
    if (!mounted) {
      return null;
    }
    setState(() {
      isToggling = false;
      pendingToggleNodeId = null;
    });
    return null;
  }
}