startDrag method

bool startDrag({
  1. required TKey key,
  2. required ReorderRenderPort<TKey> renderPort,
  3. required ScrollableState scrollable,
  4. required Offset pointerGlobal,
  5. int depthForPointerX(
    1. double sliverLocalX
    )?,
  6. double proxyCrossOffset()?,
  7. bool makeRoom = false,
  8. bool settleFromRelease = false,
})

Begins a drag session for key.

renderPort is the render surface currently displaying treeController (the RenderSliverTree). scrollable is the ancestor scrollable whose viewport clips the tree, used for pointer-to-scroll-space conversion and autoscroll.

Returns true when the session started. Returns false, starting nothing, when canReorder refuses key, or when renderPort has not been laid out yet (no painted rows to resolve against). A policy refusal is a normal runtime answer, not misuse, so it is a return value rather than an exception.

depthForPointerX maps the pointer's sliver-local x to an UNCLAMPED preferred depth (e.g. x ~/ indentWidth, since the widget layer owns the pixel constant); the resolver clamps it to the legal candidate chain when a below-zone drop sits at a subtree right-boundary. Omit it to always resolve at the deepest legal level.

proxyCrossOffset reports the floating drag proxy's VISUAL cross offset, in sliver cross space, sampled at settle-glide install time (release, cancel, or the dead-commit fallback) so the proxy-to-row handoff starts exactly where the card visually is, even when released mid-animation. Presentation-supplied, following the depthForPointerX precedent. Omit it (imperative drags with no proxy) and the glides carry no x motion while the commit baseline preserves the captured x, the classic y-only handoff. Only consulted when settleFromRelease builds a settler.

When makeRoom AND settleFromRelease are BOTH set (touch-first make-room mode), slot resolution probes at the PROXY MIDPOINT (pointer + rowExtent/2 - grabDy, a session constant) instead of the raw pointer: on touch there is no visible cursor, so selection tracks the card in hand regardless of where it was grabbed. Every other configuration resolves at the raw pointer.

Throws ArgumentError for genuine wiring misuse: renderPort not driven by treeController (cross-controller drag is out of scope).

Implementation

bool startDrag({
  required TKey key,
  required ReorderRenderPort<TKey> renderPort,
  required ScrollableState scrollable,
  required Offset pointerGlobal,
  int Function(double sliverLocalX)? depthForPointerX,
  double Function()? proxyCrossOffset,
  bool makeRoom = false,
  bool settleFromRelease = false,
}) {
  if (!renderPort.drivesController(treeController)) {
    throw ArgumentError.value(
      renderPort,
      "renderPort",
      "renderPort must be driven by the same TreeController passed to "
          "TreeReorderController. Cross-controller drag is not supported.",
    );
  }
  if (!renderPort.isLaidOut) {
    return false;
  }
  if (canReorder != null && !canReorder!(key)) {
    return false;
  }
  if (_session != null) {
    cancelDrag();
  }
  final pointerSpace = PointerSpace<TKey>(
    scrollable: scrollable,
    renderPort: renderPort,
  );
  final startSample = pointerSpace.sample(pointerGlobal);
  if (startSample == null) {
    // The scrollable is unmounted or its viewport is detached: there
    // is nothing to drag within, so refuse like any other policy check.
    return false;
  }
  // The probe is the single owner of grab geometry + probeDy; capture
  // runs once here against the start sample.
  final probe = DragProbe<TKey>(
    renderPort: renderPort,
    resolver: _resolver,
    draggedKey: key,
    depthForPointerX: depthForPointerX,
  );
  probe.captureGrab(
    start: startSample,
    midpointProbe: makeRoom && settleFromRelease,
  );
  // Per-session animation resolution: the tree controller's style is
  // read ONCE here: a mid-drag restyle never retimes a live session;
  // the next drag picks it up.
  final style = treeController.animationStyle;
  final session = DragSession<TKey>(
    draggedKey: key,
    renderPort: renderPort,
    pointerSpace: pointerSpace,
    probe: probe,
    pointerGlobal: pointerGlobal,
    commitSlideSpec: style.reorderSlide,
  );
  // Behavior collaborators capture the session's identity in their
  // callbacks, hence assignment after construction.
  session.autoScroller = AutoScroller<TKey>(
    vsync: _vsync,
    space: pointerSpace,
    pointerGlobal: () => session.pointerGlobal,
    edgeZone: autoScrollEdgeZone,
    maxVelocity: autoScrollMaxVelocity,
  );
  session.dwell = DwellExpander<TKey>(
    treeController: treeController,
    delay: autoExpandDelay,
    sessionLive: () => identical(_session, session),
    requestResolve: () => _resolveAndNotify(session),
  );
  if (makeRoom) {
    session.makeRoomDriver = MakeRoomDriver<TKey>(
      treeController: treeController,
      draggedKey: key,
      duration: style.effectiveMakeRoom.duration,
      curve: style.effectiveMakeRoom.curve,
    );
  }
  if (settleFromRelease) {
    session.settler = DropSettler<TKey>(
      treeController: treeController,
      space: pointerSpace,
      pointerGlobal: () => session.pointerGlobal,
      grabDy: () => probe.grabDy,
      draggedKey: key,
      duration: style.effectiveDropSettle.duration,
      curve: style.effectiveDropSettle.curve,
      proxyCrossOffset: proxyCrossOffset,
    );
  }
  _dragGeneration++;
  _session = session;
  // Pin the dragged row against stale eviction for the session's
  // lifetime: the drag gesture's recognizer lives on the row's own
  // `State`, so autoscrolling it out of the cache region would otherwise
  // evict the row, its `Drag`'s end/cancel would never fire, and the
  // session (plus the autoscroll ticker) would run forever.
  renderPort.pinNode(key);
  session.subscribeScroll(scrollable.position, _onScrollPositionChanged);
  _pointerPosition.value = pointerGlobal;
  // One choreography site: probe + resolver + every behavior. The
  // make-room gap of a session born over a valid slot opens here, and
  // the edge-zone-at-start autoscroll evaluation runs here too.
  session.resolve();
  // Drag session just started; currentTarget may have become non-null.
  notifyListeners();
  return true;
}