feat: snap launcher to stippled edge alignment guides

This commit is contained in:
2026-10-12 12:25:18 +02:00
parent 97420f51e1
commit dc7cf07204
4 changed files with 101 additions and 12 deletions
+30 -9
View File
@@ -1,4 +1,5 @@
import AppKit
import ConjureKit
@MainActor
final class LauncherAlignmentGuides {
@@ -16,21 +17,38 @@ final class LauncherAlignmentGuides {
return panel
}()
private var dragging = false
private var updating = false
private var dragOrigin = NSPoint.zero
private var mouseOrigin = NSPoint.zero
func begin(for window: NSWindow) {
guard window.screen != nil else { return }
guard !dragging, window.screen != nil else { return }
dragging = true
dragOrigin = window.frame.origin
mouseOrigin = NSEvent.mouseLocation
update(for: window)
overlay.level = window.level
overlay.order(.below, relativeTo: window.windowNumber)
}
func update(for window: NSWindow) {
guard dragging, let screen = window.screen else { return }
guard dragging, !updating, let screen = window.screen else { return }
updating = true
defer { updating = false }
let frame = screen.visibleFrame
let mouse = NSEvent.mouseLocation
let proposed = NSRect(
origin: NSPoint(x: dragOrigin.x + mouse.x - mouseOrigin.x, y: dragOrigin.y + mouse.y - mouseOrigin.y),
size: window.frame.size
)
let alignment = LauncherAlignment(windowFrame: proposed, visibleFrame: frame)
if window.frame.origin != alignment.snappedOrigin {
window.setFrameOrigin(alignment.snappedOrigin)
}
if overlay.frame != frame {
overlay.setFrame(frame, display: false)
}
view.targetFrame = alignment.targetFrame.offsetBy(dx: -frame.minX, dy: -frame.minY)
view.launcherFrame = window.frame.offsetBy(dx: -frame.minX, dy: -frame.minY)
view.alignmentTolerance = 1 / screen.backingScaleFactor
view.needsDisplay = true
@@ -46,6 +64,7 @@ final class LauncherAlignmentGuides {
@MainActor
private final class AlignmentGuideView: NSView {
var launcherFrame = NSRect.zero
var targetFrame = NSRect.zero
var alignmentTolerance: CGFloat = 0.5
private static let dash: [CGFloat] = [1, 5]
private static let muted = SunsetPalette.accent.withAlphaComponent(0.4).cgColor
@@ -59,23 +78,25 @@ private final class AlignmentGuideView: NSView {
context.saveGState()
defer { context.restoreGState() }
context.addRect(bounds)
context.addRect(launcherFrame.insetBy(dx: -8, dy: -8))
context.addRect(launcherFrame.insetBy(dx: 0.75, dy: 0.75))
context.clip(using: .evenOdd)
context.setLineWidth(1.5)
context.setLineCap(.round)
context.setLineDash(phase: 0, lengths: Self.dash)
context.setShadow(offset: .zero, blur: 2, color: Self.shadow)
let centeredX = abs(launcherFrame.midX - bounds.midX) <= alignmentTolerance
let centeredX = abs(launcherFrame.minX - targetFrame.minX) <= alignmentTolerance
context.setStrokeColor(centeredX ? Self.aligned : Self.muted)
context.move(to: NSPoint(x: bounds.midX, y: bounds.minY))
context.addLine(to: NSPoint(x: bounds.midX, y: bounds.maxY))
context.move(to: NSPoint(x: targetFrame.minX, y: bounds.minY))
context.addLine(to: NSPoint(x: targetFrame.minX, y: bounds.maxY))
context.move(to: NSPoint(x: targetFrame.maxX, y: bounds.minY))
context.addLine(to: NSPoint(x: targetFrame.maxX, y: bounds.maxY))
context.strokePath()
let centeredY = abs(launcherFrame.midY - bounds.midY) <= alignmentTolerance
let centeredY = abs(launcherFrame.maxY - targetFrame.maxY) <= alignmentTolerance
context.setStrokeColor(centeredY ? Self.aligned : Self.muted)
context.move(to: NSPoint(x: bounds.minX, y: bounds.midY))
context.addLine(to: NSPoint(x: bounds.maxX, y: bounds.midY))
context.move(to: NSPoint(x: bounds.minX, y: targetFrame.maxY))
context.addLine(to: NSPoint(x: bounds.maxX, y: targetFrame.maxY))
context.strokePath()
}
}