From 0645d8df4d6eb72d34c5ad934fb0c8dea35269ef Mon Sep 17 00:00:00 2001 From: arzafran Date: Thu, 15 Oct 2026 14:19:25 -0300 Subject: [PATCH 1/2] fix: stop clicks in the main window from starting a native window drag Since #413 made the main window movable, a drag on the sidebar resizer band moved the window instead of resizing the sidebar. Keep the main window immovable while AppKit dispatches each left mouse-down, the same way the folder-icon guard already did, and restore it afterwards so the Window menu tiling items still validate. --- CHANGELOG.md | 1 + Sources/WindowSwizzles.swift | 43 +++++++++--------------------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f6170f..7bc1606d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Programa is a fork of [cmux](https://github.com/manaflow-ai/cmux); for history p - The `programa codex install-hooks` and `uninstall-hooks` legacy aliases. Use `programa codex install-integration` and `uninstall-integration`. ### Fixed +- Dragging the sidebar edge resizes the sidebar again instead of moving the whole window. - The Window menu's Fill, Center, Move & Resize, Full Screen Tile and Move to Display items, and their shortcuts, work again. The main window had been marked not movable, which makes macOS disable all of them. - The notifications popover now opens in minimal mode, the default layout. Show Notifications (⌘I), the sidebar bell and the menu bar icon's Show Notifications did nothing there. - Port chips in the sidebar now show up again for terminals restored after relaunching Programa. The restored terminals kept their running shell, but port scanning never learned about them. diff --git a/Sources/WindowSwizzles.swift b/Sources/WindowSwizzles.swift index a186a097..819a6079 100644 --- a/Sources/WindowSwizzles.swift +++ b/Sources/WindowSwizzles.swift @@ -165,8 +165,14 @@ extension NSWindow { return } - guard shouldSuppressWindowMoveForFolderDrag(window: self, event: event), - let contentView = self.contentView else { + // The window stays isMovable so AppKit validates the Window menu tiling + // items, but no click may start a native window drag: one on the SwiftUI + // sidebar resizer band moved the window instead of resizing the sidebar, + // and folder-icon drags dragged the window along. Every deliberate drag + // surface (WindowDragHandleView, the titlebar background above) re-enables + // movability around its own performDrag via withTemporaryWindowMovableEnabled. + guard event.type == .leftMouseDown, isMovable, + contentView is MainWindowHostingViewMarker else { #if DEBUG if event.type == .keyDown { folderGuardMs = (ProcessInfo.processInfo.systemUptime - folderGuardStart) * 1000.0 @@ -179,39 +185,10 @@ extension NSWindow { programa_sendEvent(event) return } -#if DEBUG - if event.type == .keyDown { - folderGuardMs = (ProcessInfo.processInfo.systemUptime - folderGuardStart) * 1000.0 - } - let originalDispatchStart = event.type == .keyDown ? ProcessInfo.processInfo.systemUptime : 0 -#endif - - let contentPoint = contentView.convert(event.locationInWindow, from: nil) - let hitView = contentView.hitTest(contentPoint) - let previousMovableState = isMovable - if previousMovableState { - isMovable = false - } - - #if DEBUG - let hitDesc = hitView.map { String(describing: type(of: $0)) } ?? "nil" - dlog("window.sendEvent.folderDown suppress=1 hit=\(hitDesc) wasMovable=\(previousMovableState)") - #endif + isMovable = false programa_sendEvent(event) -#if DEBUG - if event.type == .keyDown { - originalDispatchMs = (ProcessInfo.processInfo.systemUptime - originalDispatchStart) * 1000.0 - } -#endif - - if previousMovableState { - isMovable = previousMovableState - } - - #if DEBUG - dlog("window.sendEvent.folderDown restore nowMovable=\(isMovable)") - #endif + isMovable = true } @objc func programa_performKeyEquivalent(with event: NSEvent) -> Bool { From baaafbdf3ba30b5120c49804032547da6693e379 Mon Sep 17 00:00:00 2001 From: arzafran Date: Thu, 15 Oct 2026 14:52:05 -0300 Subject: [PATCH 2/2] test: deliver the movability click through a shown key window An unshown window dropped the synthetic mouse-down before hit-testing, so the recorder never saw it, and NSHostingView does not route clicks to raw AppKit subviews. Show the window and host the recorder via NSViewRepresentable. --- programaTests/WindowAndDragTests.swift | 34 ++++++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/programaTests/WindowAndDragTests.swift b/programaTests/WindowAndDragTests.swift index 229c95f7..93351adb 100644 --- a/programaTests/WindowAndDragTests.swift +++ b/programaTests/WindowAndDragTests.swift @@ -1571,7 +1571,17 @@ final class WindowMoveSuppressionHitPathTests: XCTestCase { } } - private func movabilityDuringMouseDown(contentView: NSView) -> (during: Bool?, after: Bool) { + private struct MovabilityRecordingRepresentable: NSViewRepresentable { + let view: MovabilityRecordingView + func makeNSView(context: Context) -> MovabilityRecordingView { view } + func updateNSView(_ nsView: MovabilityRecordingView, context: Context) {} + } + + /// The click only reaches the recorder once the window is on screen and key; + /// an unshown window drops it before hit-testing. + private func movabilityDuringMouseDown( + contentView: (MovabilityRecordingView) -> NSView + ) -> (during: Bool?, after: Bool) { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 180), styleMask: [.titled, .closable], @@ -1579,25 +1589,33 @@ final class WindowMoveSuppressionHitPathTests: XCTestCase { defer: false ) defer { window.orderOut(nil) } - window.contentView = contentView + let recorder = MovabilityRecordingView(frame: NSRect(x: 0, y: 0, width: 320, height: 180)) + window.contentView = contentView(recorder) window.isMovable = true - let view = MovabilityRecordingView(frame: NSRect(x: 0, y: 0, width: 80, height: 60)) - contentView.addSubview(view) - window.sendEvent(makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 40, y: 30), window: window)) - return (view.movableDuringMouseDown, window.isMovable) + window.makeKeyAndOrderFront(nil) + window.displayIfNeeded() + window.contentView?.layoutSubtreeIfNeeded() + window.sendEvent(makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 160, y: 90), window: window)) + return (recorder.movableDuringMouseDown, window.isMovable) } /// A movable main window must not let a plain click start a native window drag: /// the sidebar resizer band moved the window instead of resizing the sidebar. func testMainWindowMouseDownDispatchKeepsWindowImmovableThenRestoresMovable() { - let result = movabilityDuringMouseDown(contentView: MainWindowHostingView(rootView: EmptyView())) + let result = movabilityDuringMouseDown { recorder in + MainWindowHostingView(rootView: MovabilityRecordingRepresentable(view: recorder)) + } XCTAssertEqual(result.during, false) XCTAssertTrue(result.after) } /// Other windows (Settings, Debug windows) keep native window dragging. func testNonMainWindowMouseDownDispatchLeavesWindowMovable() { - let result = movabilityDuringMouseDown(contentView: NSView()) + let result = movabilityDuringMouseDown { recorder in + let container = NSView(frame: recorder.frame) + container.addSubview(recorder) + return container + } XCTAssertEqual(result.during, true) XCTAssertTrue(result.after) }