feat: show stippled centering guides while dragging launcher
This commit is contained in:
@@ -39,7 +39,7 @@ Open `Package.swift` in Xcode to edit the project. Use the bundled app above to
|
|||||||
- Standard macOS text-editing shortcuts work in the search field.
|
- Standard macOS text-editing shortcuts work in the search field.
|
||||||
- Command-comma opens the native Settings window.
|
- Command-comma opens the native Settings window.
|
||||||
- Right-click the launcher background for Settings and Quit.
|
- Right-click the launcher background for Settings and Quit.
|
||||||
- Drag the panel background to move it.
|
- Drag the panel background to move it. Stippled screen-center guides appear during dragging; each axis brightens when the launcher is centered within one physical pixel. Guides use the current display's usable area, exclude the launcher itself, and disappear on release or dismissal. Positioning remains manual, without snapping.
|
||||||
|
|
||||||
Conjure runs as a native accessory app: no Dock icon, app-switcher entry, or menu-bar menus. Keyboard menu shortcuts remain available while its window is focused.
|
Conjure runs as a native accessory app: no Dock icon, app-switcher entry, or menu-bar menus. Keyboard menu shortcuts remain available while its window is focused.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import AppKit
|
||||||
|
|
||||||
|
@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 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
|
||||||
|
if overlay.frame != frame {
|
||||||
|
overlay.setFrame(frame, display: false)
|
||||||
|
}
|
||||||
|
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 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: -8, dy: -8))
|
||||||
|
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
|
||||||
|
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.strokePath()
|
||||||
|
|
||||||
|
let centeredY = abs(launcherFrame.midY - bounds.midY) <= 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.strokePath()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
private var discoveryTask: Task<Void, Never>?
|
private var discoveryTask: Task<Void, Never>?
|
||||||
private var isLaunching = false
|
private var isLaunching = false
|
||||||
private var previousApplication: NSRunningApplication?
|
private var previousApplication: NSRunningApplication?
|
||||||
|
private var alignmentGuides: LauncherAlignmentGuides?
|
||||||
|
|
||||||
init(history: LaunchHistory = LaunchHistory()) {
|
init(history: LaunchHistory = LaunchHistory()) {
|
||||||
self.history = history
|
self.history = history
|
||||||
@@ -39,6 +40,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
window.delegate = self
|
window.delegate = self
|
||||||
window.navigateSelection = { [weak self] offset in self?.moveSelection(by: offset) }
|
window.navigateSelection = { [weak self] offset in self?.moveSelection(by: offset) }
|
||||||
window.dismiss = { [weak self] in self?.dismiss() }
|
window.dismiss = { [weak self] in self?.dismiss() }
|
||||||
|
window.dragEnded = { [weak self] in self?.alignmentGuides?.end() }
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@@ -72,6 +74,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
|
|
||||||
func dismiss(restoringFocus: Bool = true) {
|
func dismiss(restoringFocus: Bool = true) {
|
||||||
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
||||||
|
alignmentGuides?.end()
|
||||||
let previous = previousApplication
|
let previous = previousApplication
|
||||||
previousApplication = nil
|
previousApplication = nil
|
||||||
window?.orderOut(nil)
|
window?.orderOut(nil)
|
||||||
@@ -81,6 +84,18 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func windowWillMove(_ notification: Notification) {
|
||||||
|
guard let window else { return }
|
||||||
|
let guides = alignmentGuides ?? LauncherAlignmentGuides()
|
||||||
|
alignmentGuides = guides
|
||||||
|
guides.begin(for: window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowDidMove(_ notification: Notification) {
|
||||||
|
guard let window else { return }
|
||||||
|
alignmentGuides?.update(for: window)
|
||||||
|
}
|
||||||
|
|
||||||
func windowDidResignKey(_ notification: Notification) {
|
func windowDidResignKey(_ notification: Notification) {
|
||||||
guard !isLaunching, window?.attachedSheet == nil else { return }
|
guard !isLaunching, window?.attachedSheet == nil else { return }
|
||||||
dismiss(restoringFocus: false)
|
dismiss(restoringFocus: false)
|
||||||
@@ -473,6 +488,7 @@ private final class LauncherPanel: NSPanel {
|
|||||||
override var canBecomeMain: Bool { true }
|
override var canBecomeMain: Bool { true }
|
||||||
var navigateSelection: ((Int) -> Void)?
|
var navigateSelection: ((Int) -> Void)?
|
||||||
var dismiss: (() -> Void)?
|
var dismiss: (() -> Void)?
|
||||||
|
var dragEnded: (() -> Void)?
|
||||||
|
|
||||||
override func sendEvent(_ event: NSEvent) {
|
override func sendEvent(_ event: NSEvent) {
|
||||||
if event.type == .keyDown, attachedSheet == nil,
|
if event.type == .keyDown, attachedSheet == nil,
|
||||||
@@ -489,6 +505,9 @@ private final class LauncherPanel: NSPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
super.sendEvent(event)
|
super.sendEvent(event)
|
||||||
|
if event.type == .leftMouseDown || event.type == .leftMouseUp {
|
||||||
|
dragEnded?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func performClose(_ sender: Any?) {
|
override func performClose(_ sender: Any?) {
|
||||||
|
|||||||
Reference in New Issue
Block a user