Repository navigation
Make sidebar empty area a window-drag region #9212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
kenifxyz
wants to merge
16
commits into
manaflow-ai:main
from
kenifxyz:feature/sidebar-empty-area-window-drag
Closed
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
5d23d6a
Make sidebar empty area a window-drag region
kenifxyz a8e3111
Keep the full-height sidebar background a SwiftUI hit target
kenifxyz 3147154
Address sidebar window-drag review feedback
7f0f9bf
Test replayed mouse-up payload
23639bb
Separate sidebar drag ownership types
722cd35
Make replay timestamp assertion precision-aware
79211a8
Eliminate sidebar drag Swift warnings
cbb5efc
Remove unreachable SwiftUI sidebar drag bridge
ba98a0b
Complete sidebar drag event lifecycle
1973c2b
Split sidebar drag lifecycle types
02ff8ba
Clarify sidebar drag event ownership
0301195
Test invalid sidebar drag thresholds
c001376
Reject invalid sidebar drag thresholds
7ad2125
Gate sidebar drag APIs for older toolchains
86fd425
Reconcile sidebar drag branch with current main
austinywang 04d2929
Keep sidebar drag policy platform-safe
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
Sources/Sidebar/SidebarEmptyAreaWindowDragController.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import AppKit | ||
|
|
||
| /// Coordinates window dragging from empty sidebar space. | ||
| /// | ||
| /// Tracking runs synchronously from `mouseDown` with `nextEvent(matching:)`, | ||
| /// the same shape as ``SidebarDividerTrackingView``, rather than through an | ||
| /// `NSGestureRecognizer`. `NSWindow.performDrag(with:)` runs its own modal | ||
| /// tracking loop and consumes the terminating mouse-up, so a recognizer that | ||
| /// calls it never receives the event that would drive it back to `.possible`: | ||
| /// it stays parked in a terminal state, `reset()` never runs, and it silently | ||
| /// stops recognizing for the rest of the window's life. | ||
| @MainActor | ||
| struct SidebarEmptyAreaWindowDragController { | ||
| /// Pointer travel, in window points, before a press becomes a window drag. | ||
| private static let dragThreshold: CGFloat = 4 | ||
| private let nextEvent: @MainActor (NSWindow) -> SidebarEmptyAreaWindowDragTrackingEvent? | ||
|
|
||
| /// Creates a controller with an injected event source. | ||
| /// | ||
| /// The default event source blocks in `.eventTracking` until the press | ||
| /// resolves into either movement or a mouse-up. | ||
| init( | ||
| nextEvent: @escaping @MainActor (NSWindow) -> SidebarEmptyAreaWindowDragTrackingEvent? = { window in | ||
| var eventMask: NSEvent.EventTypeMask = [.leftMouseDragged, .leftMouseUp] | ||
| #if compiler(>=6.2) | ||
| if #available(macOS 26.0, *) { | ||
| eventMask.insert(.mouseCancelled) | ||
| } | ||
| #endif | ||
| guard let event = window.nextEvent( | ||
| matching: eventMask, | ||
| until: .distantFuture, | ||
| inMode: .eventTracking, | ||
| dequeue: true | ||
| ) else { return nil } | ||
|
|
||
| #if compiler(>=6.2) | ||
| if #available(macOS 26.0, *), event.type == .mouseCancelled { | ||
| return .cancelled | ||
| } | ||
| #endif | ||
| if event.type == .leftMouseUp { | ||
| return .mouseUp(event) | ||
| } | ||
| return .dragged(location: event.locationInWindow) | ||
| } | ||
| ) { | ||
| self.nextEvent = nextEvent | ||
| } | ||
|
|
||
| /// Consumes `event` as a window drag when the press turns into one. | ||
| /// | ||
| /// Returns `.dragged` when AppKit moved the window and `.cancelled` when | ||
| /// the system ended the sequence without a mouse-up; callers consume both. | ||
| /// Returns `.passThrough` for a press that never passed the configured drag | ||
| /// threshold, having pushed the terminating mouse-up back onto the queue | ||
| /// first — `NSTableView`'s own `mouseDown` tracking loop waits for that | ||
| /// event, and swallowing it would hang the click. | ||
| func perform( | ||
| with event: NSEvent, | ||
| in view: NSView | ||
| ) -> SidebarEmptyAreaWindowDragOutcome { | ||
| guard let window = view.window else { return .passThrough } | ||
| guard !isWindowDragSuppressed(window: window) else { return .passThrough } | ||
|
|
||
| let start = event.locationInWindow | ||
|
|
||
| while let next = nextEvent(window) { | ||
| switch next { | ||
| case let .mouseUp(mouseUp): | ||
| window.postEvent(mouseUp, atStart: true) | ||
| return .passThrough | ||
| case .cancelled: | ||
| return .cancelled | ||
| case let .dragged(location): | ||
| let distance = hypot(location.x - start.x, location.y - start.y) | ||
| guard distance >= Self.dragThreshold else { continue } | ||
|
|
||
| withTemporaryWindowMovableEnabled(window: window) { | ||
| // AppKit requires the original mouse-down event; the | ||
| // tracked location only decides when to hand off. | ||
| window.performDrag(with: event) | ||
| } | ||
| return .dragged | ||
| } | ||
| } | ||
|
|
||
| // A source that ends without mouse-up cannot safely fall through to | ||
| // AppKit's tracking loop, which would wait for an event that may never | ||
| // arrive. Treat it as a consumed cancellation. | ||
| return .cancelled | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /// Result of tracking one empty-area mouse-down sequence. | ||
| enum SidebarEmptyAreaWindowDragOutcome: Equatable { | ||
| /// The caller should continue its normal `mouseDown` handling. | ||
| case passThrough | ||
| /// AppKit took ownership of the sequence to move the window. | ||
| case dragged | ||
| /// AppKit ended the sequence without a mouse-up to replay. | ||
| case cancelled | ||
| } |
11 changes: 11 additions & 0 deletions
11
Sources/Sidebar/SidebarEmptyAreaWindowDragTrackingEvent.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import AppKit | ||
|
|
||
| /// Event-source value consumed by the synchronous empty-area tracking loop. | ||
| enum SidebarEmptyAreaWindowDragTrackingEvent { | ||
| /// Pointer movement that may cross the drag threshold. | ||
| case dragged(location: NSPoint) | ||
| /// The terminating event that normal click handling still needs. | ||
| case mouseUp(NSEvent) | ||
| /// A system cancellation that terminates the sequence without mouse-up. | ||
| case cancelled | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.