103 lines
4.1 KiB
Swift
103 lines
4.1 KiB
Swift
import AppKit
|
|
import ConjureKit
|
|
|
|
@MainActor
|
|
final class LauncherAlignmentGuides {
|
|
private let view = AlignmentGuideView()
|
|
private lazy var overlay: NSPanel = {
|
|
let panel = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
|
|
panel.backgroundColor = .clear
|
|
panel.isOpaque = false
|
|
panel.hasShadow = false
|
|
panel.ignoresMouseEvents = true
|
|
panel.isReleasedWhenClosed = false
|
|
panel.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary]
|
|
panel.contentView = view
|
|
panel.setAccessibilityElement(false)
|
|
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 }
|
|
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
|
|
}
|
|
|
|
func end() {
|
|
guard dragging else { return }
|
|
dragging = false
|
|
overlay.orderOut(nil)
|
|
}
|
|
}
|
|
|
|
@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
|
|
private static let aligned = SunsetPalette.accent.withAlphaComponent(0.95).cgColor
|
|
private static let shadow = NSColor.black.withAlphaComponent(0.6).cgColor
|
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
|
context.saveGState()
|
|
defer { context.restoreGState() }
|
|
context.addRect(bounds)
|
|
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.minX - targetFrame.minX) <= alignmentTolerance
|
|
context.setStrokeColor(centeredX ? 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.move(to: NSPoint(x: bounds.minX, y: targetFrame.maxY))
|
|
context.addLine(to: NSPoint(x: bounds.maxX, y: targetFrame.maxY))
|
|
context.strokePath()
|
|
}
|
|
}
|