Nested NSHostingController layers (from bonsplit's SinglePaneWrapper) prevent AppKit's NSDraggingDestination routing from reaching terminal views. Install a transparent FileDropOverlayView on the window's theme frame that intercepts file drags and forwards drops to the GhosttyNSView under the cursor. Mouse events pass through via a hide-send-unhide pattern. Fix y-axis inversion in split targeting: hitTest expects coordinates in the receiver's superview's coordinate system, not the receiver's own. Converting to contentView's coords flipped y because NSHostingView is flipped, causing top/bottom split drops to land in the wrong terminal. Also adds bonsplit onFileDrop API, PaneDragContainerView, and drop_hit_test socket command for testing coordinate-to-terminal mapping.
167 lines
6 KiB
Python
167 lines
6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Regression test: file drops in vertical splits target the correct terminal.
|
|
|
|
When the window has a vertical split (top/bottom), a file drop over the top
|
|
terminal must be routed to the top terminal (not the bottom one), and vice
|
|
versa. A coordinate-system bug (y-axis inversion in hitTest) previously
|
|
caused drops to land in the wrong pane.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from cmux import cmux
|
|
|
|
|
|
def surface_ids_from_layout(layout: dict):
|
|
"""Extract panel IDs keyed by vertical position from layout_debug output.
|
|
|
|
Returns (top_surface_id, bottom_surface_id) based on pane frame y-origins.
|
|
Bonsplit's pane frames use a top-left origin (flipped) coordinate system,
|
|
so smaller y = higher on screen = top pane.
|
|
"""
|
|
panels = layout.get("selectedPanels", [])
|
|
if len(panels) < 2:
|
|
return None, None
|
|
|
|
def y_origin(p):
|
|
frame = p.get("paneFrame")
|
|
if frame is None:
|
|
return 0
|
|
return frame.get("y", 0)
|
|
|
|
# Sort ascending by y: smallest y = top pane visually
|
|
sorted_panels = sorted(panels, key=y_origin)
|
|
top_id = sorted_panels[0].get("panelId")
|
|
bottom_id = sorted_panels[1].get("panelId")
|
|
return top_id, bottom_id
|
|
|
|
|
|
def main() -> int:
|
|
with cmux() as client:
|
|
try:
|
|
client.activate_app()
|
|
except Exception:
|
|
pass
|
|
|
|
# Start with a single terminal surface.
|
|
surfaces = client.list_surfaces()
|
|
if not surfaces:
|
|
client.new_workspace()
|
|
time.sleep(0.3)
|
|
surfaces = client.list_surfaces()
|
|
if not surfaces:
|
|
print("FAIL: no surfaces available")
|
|
return 1
|
|
|
|
# Create a vertical (top/bottom) split.
|
|
client.new_split("down")
|
|
time.sleep(0.5)
|
|
|
|
layout = client.layout_debug()
|
|
top_panel_id, bottom_panel_id = surface_ids_from_layout(layout)
|
|
if not top_panel_id or not bottom_panel_id:
|
|
print("FAIL: could not determine top/bottom panel IDs from layout")
|
|
print(f"layout: {layout}")
|
|
return 1
|
|
|
|
if top_panel_id == bottom_panel_id:
|
|
print("FAIL: top and bottom panel IDs are the same")
|
|
return 1
|
|
|
|
# Test the hit-test mapping directly: given a point in the top/bottom
|
|
# half, does it resolve to *different* terminals in the expected order?
|
|
# drop_hit_test uses content-area coordinates: (0,0)=top-left, (1,1)=bottom-right.
|
|
|
|
# Hit-test near the vertical centre of the top pane (y ≈ 0.25).
|
|
top_hit = client.drop_hit_test(0.5, 0.25)
|
|
# Hit-test near the vertical centre of the bottom pane (y ≈ 0.75).
|
|
bottom_hit = client.drop_hit_test(0.5, 0.75)
|
|
|
|
if top_hit is None:
|
|
print("FAIL: drop_hit_test returned 'none' for top region")
|
|
return 1
|
|
if bottom_hit is None:
|
|
print("FAIL: drop_hit_test returned 'none' for bottom region")
|
|
return 1
|
|
if top_hit == bottom_hit:
|
|
print("FAIL: top and bottom hit test returned the same surface")
|
|
print(f" top_hit={top_hit} bottom_hit={bottom_hit}")
|
|
return 1
|
|
|
|
# Verify the mapping is not inverted: the top hit should correspond to
|
|
# the top pane and the bottom hit to the bottom pane.
|
|
# Cross-check via layout_debug pane frames (flipped coords: smaller y = top).
|
|
panels = layout.get("selectedPanels", [])
|
|
panel_to_y = {}
|
|
for p in panels:
|
|
pid = p.get("panelId")
|
|
frame = p.get("paneFrame")
|
|
if pid and frame:
|
|
panel_to_y[pid] = frame.get("y", 0)
|
|
|
|
# drop_hit_test returns uppercase UUIDs; panelId may differ in case.
|
|
def normalise(uuid_str):
|
|
return uuid_str.upper() if uuid_str else ""
|
|
|
|
top_y = panel_to_y.get(normalise(top_hit), panel_to_y.get(top_hit))
|
|
bottom_y = panel_to_y.get(normalise(bottom_hit), panel_to_y.get(bottom_hit))
|
|
|
|
if top_y is None or bottom_y is None:
|
|
print("FAIL: could not find hit-test surface IDs in layout panel map")
|
|
print(f" top_hit={top_hit} bottom_hit={bottom_hit}")
|
|
print(f" panel_to_y={panel_to_y}")
|
|
return 1
|
|
|
|
# In flipped coords: top pane has smaller y
|
|
if top_y >= bottom_y:
|
|
print("FAIL: y-axis is inverted — top hit resolved to bottom pane")
|
|
print(f" top_hit={top_hit} (y={top_y}) bottom_hit={bottom_hit} (y={bottom_y})")
|
|
return 1
|
|
|
|
print("PASS: vertical split drop targeting is correct")
|
|
print(f" top_hit={top_hit} bottom_hit={bottom_hit}")
|
|
|
|
# Also test horizontal split targeting.
|
|
# Close the bottom pane and create a horizontal split instead.
|
|
# First, close all extra surfaces to get back to 1.
|
|
surfaces = client.list_surfaces()
|
|
if len(surfaces) > 1:
|
|
# Focus and close the non-first surface
|
|
for _, sid, is_focused in surfaces[1:]:
|
|
try:
|
|
client.close_surface(sid)
|
|
except Exception:
|
|
pass
|
|
time.sleep(0.3)
|
|
|
|
client.new_split("right")
|
|
time.sleep(0.5)
|
|
|
|
# Hit-test left half and right half
|
|
left_hit = client.drop_hit_test(0.25, 0.5)
|
|
right_hit = client.drop_hit_test(0.75, 0.5)
|
|
|
|
if left_hit is None:
|
|
print("FAIL: drop_hit_test returned 'none' for left region")
|
|
return 1
|
|
if right_hit is None:
|
|
print("FAIL: drop_hit_test returned 'none' for right region")
|
|
return 1
|
|
if left_hit == right_hit:
|
|
print("FAIL: left and right hit test returned the same surface")
|
|
print(f" left_hit={left_hit} right_hit={right_hit}")
|
|
return 1
|
|
|
|
print("PASS: horizontal split drop targeting is correct")
|
|
print(f" left_hit={left_hit} right_hit={right_hit}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|