Files
conjure/Sources/Conjure/LauncherAlignmentGuides.swift
T

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
func begin(for window: NSWindow) {
guard !dragging, window.screen != nil else { return }
dragging = true
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 }
let frame = screen.visibleFrame
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.snapsHorizontally = alignment.snappedOrigin.x == alignment.targetFrame.minX
view.snapsVertically = alignment.snappedOrigin.y == alignment.targetFrame.minY
view.needsDisplay = true
}
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)
}
}
}
}
@MainActor
private final class AlignmentGuideView: NSView {
var targetFrame = NSRect.zero
var snapsHorizontally = false
var snapsVertically = false
private static let dash: [CGFloat] = [1, 5]
private var muted = ThemePalette.accent.withAlphaComponent(0.4).cgColor
private var aligned = ThemePalette.accent.withAlphaComponent(0.95).cgColor
private static let shadow = NSColor.black.withAlphaComponent(0.6).cgColor
private var themeObservation: ThemeObservation?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
themeObservation = ThemeObservation { [weak self] in
self?.muted = ThemePalette.accent.withAlphaComponent(0.4).cgColor
self?.aligned = ThemePalette.accent.withAlphaComponent(0.95).cgColor
self?.needsDisplay = true
}
}
required init?(coder: NSCoder) { fatalError("Conjure creates alignment guides programmatically") }
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.clip()
context.setLineWidth(1.5)
context.setLineCap(.round)
context.setLineDash(phase: 0, lengths: Self.dash)
context.setShadow(offset: .zero, blur: 2, color: Self.shadow)
context.setStrokeColor(snapsHorizontally ? aligned : 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()
context.setStrokeColor(snapsVertically ? aligned : muted)
context.move(to: NSPoint(x: bounds.minX, y: targetFrame.maxY))
context.addLine(to: NSPoint(x: bounds.maxX, y: targetFrame.maxY))
context.strokePath()
}
}