Skip to content

Add whole-tab drop targets when moving a split pane - #742

Open
taherod wants to merge 2 commits into
gnachman:masterfrom
taherod:tab-edge-drop-targets
Open

taherod wants to merge 2 commits into
gnachman:masterfrom
taherod:tab-edge-drop-targets

Conversation

@taherod

@taherod taherod commented Sep 1, 2026

Copy link
Copy Markdown

Problem

Every drop target for a moved pane splits the hovered pane's splitter. SplitSelectionView offers four halves inside the pane under the pointer, and -[MovePaneController reallyDropInSession:half:atPoint:] turns that into splitVertically:before:addingSession:targetSession:, which splits the target session's parent splitter.

So with N panes side by side, there is no way to move a pane so it spans the whole top, bottom, left, or right of the tab: dropping on the north half of pane 3 gives a pane as wide as pane 3's column only. That placement requires a split at the tab's root splitter, which nothing exposes (outside tmux, where layouts can do it).

Change

Three panes side by side; dragging one near the top edge of the tab, then near the right edge:

Moving a pane to the top and right edges of a tab

Dragging a pane within tabEdgeDropZoneSize points (default 24) of a tab's outer edge now offers a whole-tab drop target. The dropped pane becomes a child of the root splitter, spanning the tab's full width (top/bottom) or full height (left/right).

The same targets work in Session > Move Session to Split Pane, since both the drag and click-to-move paths go through -[SplitSelectionView updateAtPoint:].

Details worth reviewing:

  • New halves. SplitSessionHalf gains kTabTopEdge/kTabBottomEdge/kTabLeftEdge/kTabRightEdge plus three predicate functions. Every existing switch over the enum is exhaustive and was extended.
  • Eligibility. A pane offers an edge only when that edge is also the tab's edge. -[PTYTab tabEdgesForSession:] compares frames in window coordinates, because split views are flipped and root's own space would invert top and bottom. Refused for tmux tabs (no tmux command moves a pane to a window edge), for a maximized pane, for a locked layout, and for a single-pane tab (where an edge drop equals a half drop).
  • Zone behavior. The zone is clamped to a third of the pane's extent so half-pane drops stay reachable in a short or narrow pane, and it has 6pt of hysteresis so a pointer on the boundary does not flicker between a whole-tab and a half-pane target.
  • Drawing. The highlight spans panes, and the root is a split view that can hold only panes, so the tab puts a click-through SplitSelectionView in the window's content view instead. It is sized to the share the pane will actually get, so the preview does not lie. The hovered pane's own overlay draws nothing while a tab edge is selected.
  • Tree surgery. -[PTYTab insertSession:atTabEdge:] handles the three cases the way splitVertically:newSession:before:targetSession: does for panes: root has one child (just set its orientation), root already splits along the needed axis (insert at the front or back), or root splits the other way (move its children into a new splitter that preserves their arrangement, flip the root, then insert). checkInvariants: runs before and after.
  • Sizing. A new subview arrives spanning the whole root, so -adjustSubviews would hand it about half the tab regardless of how many panes were there. -giveNewRootSubview:anEqualShareAlongVertical: gives it an equal share and scales the existing panes proportionally, keeping their relative sizes.
  • Shared insertion tail. -[PseudoTerminal splitVertically:before:addingSession:targetSession:performSetup:] had ~70 lines of post-insert bookkeeping (tab color, divorce inheritance, focus, dimming, fitting) that the edge move needs verbatim. It moved into -addSession:targetSession:performSetup:insert:, which both callers use with a block; no behavior change on the existing path.
  • Off switch. tabEdgeDropZoneSize = 0 disables the feature entirely, restoring the previous drop behavior.

Testing

  • tools/build.sh clean.
  • ModernTests/iTermSplitSelectionViewTabEdgeTests.m: 8 tests, all passing. They drive -updateAtPoint: on a bare view: each eligible edge is selected near it, an edge that is not on the tab boundary is not offered, no mask means only halves, the zone is clamped on a short pane, selection is sticky within hysteresis, and the change callback reports entry and exit exactly once each.
  • Manual: three side-by-side panes, dragged one to the top edge and another to the right edge. Log shows moveSession:… toTabEdge:6 and toTabEdge:9; the moved session went 43x38 → 137x18, i.e. full width at half height. No invariant failures in either the wrap case (top) or the same-axis case (right).

Happy to adjust the default zone size, the visuals, or split this differently if you would rather have it another way.

Dragging a pane near a tab's outer edge now offers a drop spanning the
tab's full width or height. The pane becomes a child of the root
splitter instead of splitting the hovered pane, which was the only
placement available before. Same targets in Move Session to Split Pane.
Zone size: tabEdgeDropZoneSize (0 disables). Not offered for tmux tabs.
# Conflicts:
#	sources/Settings/iTermAdvancedSettingsModel.m
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant