Skip to content

Add native-style screen edge snapping#161

Open
chid wants to merge 1 commit into
pablopunk:mainfrom
chid:feature/screen-edge-snapping-squash
Open

Add native-style screen edge snapping#161
chid wants to merge 1 commit into
pablopunk:mainfrom
chid:feature/screen-edge-snapping-squash

Conversation

@chid

@chid chid commented Jun 6, 2026

Copy link
Copy Markdown

Summary

  • replace the option-key snap preference with a single Enable snapping setting (though this option didn't exist, just compared against the PoC)
  • add native-style screen-edge snap targets for halves, quarters, and full screen from the top edge
  • add a minimal border preview overlay for the snap destination (not as nice as native)

Context

Addresses the feedback from #156 (comment).

Summary by CodeRabbit

New Features

  • Window Snapping: Added window snapping functionality that intelligently aligns windows to screen edges and corners. A native snap preview visualizes the alignment during dragging and resizing operations.
  • Snapping Preference: Introduced "Enable snapping" toggle in Preferences to easily control whether window snapping is active.

@codesandbox

codesandbox Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

@chid is attempting to deploy a commit to the Pablo Varela's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Looking for one thing? Review this PR in Change Stack to search files, summaries, diffs, and code without losing your place.

Review Change Stack

Walkthrough

This PR extends SwiftShift's window management with a configurable snapping feature. It adds a user preference to enable/disable snapping, introduces a SnapPreviewWindow overlay displayed during snap operations, and implements enhanced snapping logic during both move and resize operations. The implementation includes screen edge detection via coordinate conversion utilities, native snap frame detection near the cursor, and intelligent avoidance of snapping on interior seams between adjacent displays.

Possibly related PRs

  • pablopunk/SwiftShift#150: Both PRs modify MouseTracker's move/resize snapping logic—introducing/adjusting snapping distance and using nearby window/snap-rect geometry to compute snapped origins/sizes—so the changes are directly connected at the implementation level.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding native-style screen edge snapping capability to MouseTracker with a preview window and configurable enablement.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chid chid marked this pull request as ready for review June 6, 2026 02:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Swift` Shift/src/Manager/MouseTracker.swift:
- Around line 253-271: The loop over screenSnapRects in MouseTracker.swift is
snapping to edges of displays that are far from the cursor when only one axis
aligns; to fix, bail out early for screens that are not near the cursor by
checking proximity on both axes before computing adjacency and applying snaps:
inside the for screen in screenSnapRects loop (where cursorCG, snapDistance,
result and size are used), add a guard that ensures cursorCG.x is within
(screen.minX - snapDistance) .. (screen.maxX + snapDistance) AND cursorCG.y is
within (screen.minY - snapDistance) .. (screen.maxY + snapDistance); if not,
continue so only screens near the cursor are considered for
adjacentLeft/Right/Above/Below and result.x/result.y adjustments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcccc577-c371-42a2-8507-0fa81b0977a6

📥 Commits

Reviewing files that changed from the base of the PR and between 901243d and 458df10.

📒 Files selected for processing (4)
  • Swift Shift/src/Manager/MouseTracker.swift
  • Swift Shift/src/Manager/Preferences.swift
  • Swift Shift/src/Manager/WindowManager.swift
  • Swift Shift/src/View/PreferencesView.swift

Comment on lines +253 to +271
for screen in screenSnapRects {
// An edge shared with an adjacent display (where the cursor currently
// sits) is an interior seam, not a real screen edge. Snapping there
// traps the window at the boundary instead of letting it cross.
let adjacentLeft = screenSnapRects.contains { $0 != screen && abs($0.maxX - screen.minX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
let adjacentRight = screenSnapRects.contains { $0 != screen && abs($0.minX - screen.maxX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
let adjacentAbove = screenSnapRects.contains { $0 != screen && abs($0.maxY - screen.minY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
let adjacentBelow = screenSnapRects.contains { $0 != screen && abs($0.minY - screen.maxY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }

if !adjacentLeft, abs(cursorCG.x - screen.minX) <= snapDistance {
result.x = screen.minX
} else if !adjacentRight, abs(cursorCG.x - screen.maxX) <= snapDistance {
result.x = screen.maxX - size.width
}
if !adjacentAbove, abs(cursorCG.y - screen.minY) <= snapDistance {
result.y = screen.minY
} else if !adjacentBelow, abs(cursorCG.y - screen.maxY) <= snapDistance {
result.y = screen.maxY - size.height
}

@coderabbitai coderabbitai Bot Jun 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain edge snapping to screens near the cursor.

Line 253 iterates all screenSnapRects and can apply snaps for a different display if only one axis happens to align. On offset multi-monitor layouts this causes jumpy, incorrect snaps.

Proposed fix
     for screen in screenSnapRects {
+            guard screen.insetBy(dx: -snapDistance, dy: -snapDistance).contains(cursorCG) else { continue }
             // An edge shared with an adjacent display (where the cursor currently
             // sits) is an interior seam, not a real screen edge. Snapping there
             // traps the window at the boundary instead of letting it cross.
             let adjacentLeft = screenSnapRects.contains { $0 != screen && abs($0.maxX - screen.minX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
             let adjacentRight = screenSnapRects.contains { $0 != screen && abs($0.minX - screen.maxX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
             let adjacentAbove = screenSnapRects.contains { $0 != screen && abs($0.maxY - screen.minY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
             let adjacentBelow = screenSnapRects.contains { $0 != screen && abs($0.minY - screen.maxY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for screen in screenSnapRects {
// An edge shared with an adjacent display (where the cursor currently
// sits) is an interior seam, not a real screen edge. Snapping there
// traps the window at the boundary instead of letting it cross.
let adjacentLeft = screenSnapRects.contains { $0 != screen && abs($0.maxX - screen.minX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
let adjacentRight = screenSnapRects.contains { $0 != screen && abs($0.minX - screen.maxX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
let adjacentAbove = screenSnapRects.contains { $0 != screen && abs($0.maxY - screen.minY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
let adjacentBelow = screenSnapRects.contains { $0 != screen && abs($0.minY - screen.maxY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
if !adjacentLeft, abs(cursorCG.x - screen.minX) <= snapDistance {
result.x = screen.minX
} else if !adjacentRight, abs(cursorCG.x - screen.maxX) <= snapDistance {
result.x = screen.maxX - size.width
}
if !adjacentAbove, abs(cursorCG.y - screen.minY) <= snapDistance {
result.y = screen.minY
} else if !adjacentBelow, abs(cursorCG.y - screen.maxY) <= snapDistance {
result.y = screen.maxY - size.height
}
for screen in screenSnapRects {
guard screen.insetBy(dx: -snapDistance, dy: -snapDistance).contains(cursorCG) else { continue }
// An edge shared with an adjacent display (where the cursor currently
// sits) is an interior seam, not a real screen edge. Snapping there
// traps the window at the boundary instead of letting it cross.
let adjacentLeft = screenSnapRects.contains { $0 != screen && abs($0.maxX - screen.minX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
let adjacentRight = screenSnapRects.contains { $0 != screen && abs($0.minX - screen.maxX) <= tolerance && cursorCG.y >= $0.minY && cursorCG.y <= $0.maxY }
let adjacentAbove = screenSnapRects.contains { $0 != screen && abs($0.maxY - screen.minY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
let adjacentBelow = screenSnapRects.contains { $0 != screen && abs($0.minY - screen.maxY) <= tolerance && cursorCG.x >= $0.minX && cursorCG.x <= $0.maxX }
if !adjacentLeft, abs(cursorCG.x - screen.minX) <= snapDistance {
result.x = screen.minX
} else if !adjacentRight, abs(cursorCG.x - screen.maxX) <= snapDistance {
result.x = screen.maxX - size.width
}
if !adjacentAbove, abs(cursorCG.y - screen.minY) <= snapDistance {
result.y = screen.minY
} else if !adjacentBelow, abs(cursorCG.y - screen.maxY) <= snapDistance {
result.y = screen.maxY - size.height
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Swift` Shift/src/Manager/MouseTracker.swift around lines 253 - 271, The loop
over screenSnapRects in MouseTracker.swift is snapping to edges of displays that
are far from the cursor when only one axis aligns; to fix, bail out early for
screens that are not near the cursor by checking proximity on both axes before
computing adjacency and applying snaps: inside the for screen in screenSnapRects
loop (where cursorCG, snapDistance, result and size are used), add a guard that
ensures cursorCG.x is within (screen.minX - snapDistance) .. (screen.maxX +
snapDistance) AND cursorCG.y is within (screen.minY - snapDistance) ..
(screen.maxY + snapDistance); if not, continue so only screens near the cursor
are considered for adjacentLeft/Right/Above/Below and result.x/result.y
adjustments.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is an issue that I noticed that needs a bit of polish, but requires a lot of complexity to get right (as it would need to know the monitor layouts)

A simple solution to reduce the jerky behaviour would also be to have a dynamic guard (on position) or tolerance but fundamentally the snap behaviour and monitor move behaviour requires rework, alternatively you could have it so that snapping doesn't work on the boundaries between monitors.

In my view, I was able to 'work it out' after playing with it over a few minutes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chid, understood — thanks for the context. The guard line I proposed is actually the "dynamic guard on position" you described: it's a single check that constrains snapping to only the display the cursor is near, without requiring any knowledge of the full monitor layout. It sidesteps the cross-axis false-positive without reworking the broader snap/move logic.

That said, the existing adjacency-seam detection already handles the most important case (preventing the window from getting trapped at display boundaries), so the current behaviour is reasonable for an initial release.

If you'd like, I can open a follow-up issue to track the remaining polish (e.g. the cursor-proximity guard + any further multi-monitor move/snap rework) so it doesn't get lost.


🧠 Learnings used
Learnt from: pablopunk
Repo: pablopunk/SwiftShift PR: 150
File: Swift Shift/src/Manager/WindowManager.swift:0-0
Timestamp: 2026-05-13T17:15:02.309Z
Learning: In SwiftShift's `getVisibleWindowRects(excluding:)` (Swift Shift/src/Manager/WindowManager.swift), the exclusion filter uses exact rect equality (`rect.equalTo(excludedRect)`) to remove only the dragged window itself from snap candidates. Containing or maximized background windows must NOT be excluded — their outer edges are valid snap targets. Actual snapping is gated by perpendicular-overlap and a 10px edge-proximity check in MouseTracker, not by this exclusion filter.

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