feat: snap launcher on release at upper-third alignment
This commit is contained in:
@@ -17,47 +17,39 @@ 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 !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, !updating, let screen = window.screen else { return }
|
||||
updating = true
|
||||
defer { updating = false }
|
||||
guard dragging, let screen = window.screen else { return }
|
||||
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)
|
||||
}
|
||||
let alignment = LauncherAlignment(windowFrame: window.frame, visibleFrame: frame)
|
||||
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.snapsHorizontally = alignment.snappedOrigin.x == alignment.targetFrame.minX
|
||||
view.snapsVertically = alignment.snappedOrigin.y == alignment.targetFrame.minY
|
||||
view.needsDisplay = true
|
||||
}
|
||||
|
||||
func end() {
|
||||
func end(snapping window: NSWindow? = nil) {
|
||||
guard dragging else { return }
|
||||
dragging = false
|
||||
overlay.orderOut(nil)
|
||||
if let window, let screen = window.screen {
|
||||
let origin = LauncherAlignment(windowFrame: window.frame, visibleFrame: screen.visibleFrame).snappedOrigin
|
||||
if window.frame.origin != origin {
|
||||
window.setFrameOrigin(origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +57,8 @@ final class LauncherAlignmentGuides {
|
||||
private final class AlignmentGuideView: NSView {
|
||||
var launcherFrame = NSRect.zero
|
||||
var targetFrame = NSRect.zero
|
||||
var alignmentTolerance: CGFloat = 0.5
|
||||
var snapsHorizontally = false
|
||||
var snapsVertically = false
|
||||
private static let dash: [CGFloat] = [1, 5]
|
||||
private static let muted = SunsetPalette.accent.withAlphaComponent(0.4).cgColor
|
||||
private static let aligned = SunsetPalette.accent.withAlphaComponent(0.95).cgColor
|
||||
@@ -85,16 +78,14 @@ private final class AlignmentGuideView: NSView {
|
||||
context.setLineDash(phase: 0, lengths: Self.dash)
|
||||
context.setShadow(offset: .zero, blur: 2, color: Self.shadow)
|
||||
|
||||
let centeredX = abs(launcherFrame.minX - targetFrame.minX) <= alignmentTolerance
|
||||
context.setStrokeColor(centeredX ? Self.aligned : Self.muted)
|
||||
context.setStrokeColor(snapsHorizontally ? Self.aligned : Self.muted)
|
||||
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.maxY - targetFrame.maxY) <= alignmentTolerance
|
||||
context.setStrokeColor(centeredY ? Self.aligned : Self.muted)
|
||||
context.setStrokeColor(snapsVertically ? Self.aligned : Self.muted)
|
||||
context.move(to: NSPoint(x: bounds.minX, y: targetFrame.maxY))
|
||||
context.addLine(to: NSPoint(x: bounds.maxX, y: targetFrame.maxY))
|
||||
context.strokePath()
|
||||
|
||||
@@ -40,7 +40,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
window.delegate = self
|
||||
window.navigateSelection = { [weak self] offset in self?.moveSelection(by: offset) }
|
||||
window.dismiss = { [weak self] in self?.dismiss() }
|
||||
window.dragEnded = { [weak self] in self?.alignmentGuides?.end() }
|
||||
window.dragEnded = { [weak self] in self?.alignmentGuides?.end(snapping: self?.window) }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -505,7 +505,7 @@ private final class LauncherPanel: NSPanel {
|
||||
}
|
||||
}
|
||||
super.sendEvent(event)
|
||||
if event.type == .leftMouseDown || event.type == .leftMouseUp {
|
||||
if event.type == .leftMouseUp || (event.type == .leftMouseDown && NSEvent.pressedMouseButtons & 1 == 0) {
|
||||
dragEnded?()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public struct LauncherAlignment: Sendable {
|
||||
// Whole-point origins keep the guides aligned with AppKit's window positioning.
|
||||
targetFrame = CGRect(
|
||||
x: (visibleFrame.midX - windowFrame.width / 2).rounded(.down),
|
||||
y: (visibleFrame.midY - windowFrame.height / 2).rounded(.down),
|
||||
y: (visibleFrame.maxY - visibleFrame.height / 5 - windowFrame.height).rounded(.down),
|
||||
width: windowFrame.width,
|
||||
height: windowFrame.height
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user