fix: Mac window becomes unmovable after pane rearrangement (#10133)

On main, after rearranging panes, the window becomes permanently
immovable. Grab handles temporarily set `window.isMovable = false` on
hover to prevent window dragging from interfering with pane drags
(fixing [#10110](https://github.com/ghostty-org/ghostty/issues/10110)),
but the restoration logic failed when views were destroyed during pane
rearrangement (which happens each time a pane is rearranged).

The previous approach managed `window.isMovable` state across the view
lifecycle:

1. `mouseEntered` → saved and disabled `window.isMovable`
2. View removed during rearrangement → `mouseExited` never fired
3. `deinit` ran with `self.window` already nil → restoration failed
4. Window stuck with `isMovable = false`

Instead of managing window state, prevent the mouseDown event from
reaching the window's drag handler by overriding mouse event handling in
the grab handle view.

Per [Apple's Event Handling
Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingMouseEvents/HandlingMouseEvents.html):

> Custom NSView objects should not invoke super in their implementations
of NSResponder mouse-event-handling methods such as mouseDown:,
mouseDragged: and mouseUp: unless it is known that the inherited
implementation provides some needed functionality.

This eliminates all state management while solving both the original
issue (#10110) and the new bug.

AI disclosure: claude code found and wrote the fix. I tested it manually
to see that it works. I pressed claude quite hard here to come up with
the best fix, and looked at documentation to understand what the fix was
doing. It seems like this is a better approach overall to preventing the
main window from being dragged when grabbing the Surface Drag handle.
This commit is contained in:
Mitchell Hashimoto
2026-01-01 16:21:15 -08:00
committed by GitHub

View File

@@ -105,22 +105,30 @@ extension Ghostty {
/// Whether the current drag was cancelled by pressing escape.
private var dragCancelledByEscape: Bool = false
/// Original value of `window.isMovable` to restore
/// when the mouse exits.
private var isWindowMovable: Bool?
deinit {
if let escapeMonitor {
NSEvent.removeMonitor(escapeMonitor)
}
}
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
// Ensure this view gets the mouse event before window dragging handlers
return true
}
override func mouseDown(with event: NSEvent) {
// Consume the mouseDown event to prevent it from propagating to the
// window's drag handler. This fixes issue #10110 where grab handles
// would drag the window instead of initiating pane drags.
// Don't call super - the drag will be initiated in mouseDragged.
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
// To update our tracking area we just recreate it all.
trackingAreas.forEach { removeTrackingArea($0) }
// Add our tracking area for mouse events
addTrackingArea(NSTrackingArea(
rect: bounds,
@@ -135,18 +143,10 @@ extension Ghostty {
}
override func mouseEntered(with event: NSEvent) {
// Temporarily disable `isMovable` to fix
// https://github.com/ghostty-org/ghostty/issues/10110
isWindowMovable = window?.isMovable
window?.isMovable = false
onHoverChanged?(true)
}
override func mouseExited(with event: NSEvent) {
if let isWindowMovable {
window?.isMovable = isWindowMovable
self.isWindowMovable = nil
}
onHoverChanged?(false)
}
@@ -237,7 +237,7 @@ extension Ghostty {
NSEvent.removeMonitor(escapeMonitor)
self.escapeMonitor = nil
}
if operation == [] && !dragCancelledByEscape {
let endsInWindow = NSApplication.shared.windows.contains { window in
window.isVisible && window.frame.contains(screenPoint)
@@ -250,7 +250,7 @@ extension Ghostty {
)
}
}
isTracking = false
onDragStateChanged?(false)
}