@lattices/cli 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
|
|
4
|
+
final class WindowDragSnapController {
|
|
5
|
+
static let shared = WindowDragSnapController()
|
|
6
|
+
|
|
7
|
+
private struct DragWindowCandidate {
|
|
8
|
+
let pid: pid_t
|
|
9
|
+
let wid: UInt32?
|
|
10
|
+
let axWindow: AXUIElement
|
|
11
|
+
let initialAXFrame: CGRect
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private struct ResolvedSnapZone {
|
|
15
|
+
let id: String
|
|
16
|
+
let label: String
|
|
17
|
+
let placement: PlacementSpec
|
|
18
|
+
let screen: NSScreen
|
|
19
|
+
let screenID: String
|
|
20
|
+
let triggerRect: CGRect
|
|
21
|
+
let visibleRect: CGRect
|
|
22
|
+
let previewRect: CGRect
|
|
23
|
+
let priority: Int
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private struct DragSession {
|
|
27
|
+
let pid: pid_t
|
|
28
|
+
let wid: UInt32?
|
|
29
|
+
let zones: [ResolvedSnapZone]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private var mouseDownMonitor: Any?
|
|
33
|
+
private var mouseDragMonitor: Any?
|
|
34
|
+
private var mouseUpMonitor: Any?
|
|
35
|
+
private var flagsChangedMonitor: Any?
|
|
36
|
+
|
|
37
|
+
private var dragCandidate: DragWindowCandidate?
|
|
38
|
+
private var activeSession: DragSession?
|
|
39
|
+
private var overlayPanels: [String: WindowSnapOverlayPanel] = [:]
|
|
40
|
+
private var modifierModeEnabled = false
|
|
41
|
+
private var windowHasMoved = false
|
|
42
|
+
|
|
43
|
+
private init() {}
|
|
44
|
+
|
|
45
|
+
func start() {
|
|
46
|
+
guard mouseDownMonitor == nil,
|
|
47
|
+
mouseDragMonitor == nil,
|
|
48
|
+
mouseUpMonitor == nil,
|
|
49
|
+
flagsChangedMonitor == nil else { return }
|
|
50
|
+
|
|
51
|
+
mouseDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
|
|
52
|
+
self?.handleMouseDown(event)
|
|
53
|
+
}
|
|
54
|
+
mouseDragMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDragged) { [weak self] event in
|
|
55
|
+
self?.handleMouseDragged(event)
|
|
56
|
+
}
|
|
57
|
+
mouseUpMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseUp) { [weak self] event in
|
|
58
|
+
self?.handleMouseUp(event)
|
|
59
|
+
}
|
|
60
|
+
flagsChangedMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
|
61
|
+
self?.handleFlagsChanged(event)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
DiagnosticLog.shared.info("WindowDragSnap: global drag monitors started")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func stop() {
|
|
68
|
+
if let monitor = mouseDownMonitor { NSEvent.removeMonitor(monitor) }
|
|
69
|
+
if let monitor = mouseDragMonitor { NSEvent.removeMonitor(monitor) }
|
|
70
|
+
if let monitor = mouseUpMonitor { NSEvent.removeMonitor(monitor) }
|
|
71
|
+
if let monitor = flagsChangedMonitor { NSEvent.removeMonitor(monitor) }
|
|
72
|
+
mouseDownMonitor = nil
|
|
73
|
+
mouseDragMonitor = nil
|
|
74
|
+
mouseUpMonitor = nil
|
|
75
|
+
flagsChangedMonitor = nil
|
|
76
|
+
clearTracking()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func handleMouseDown(_ event: NSEvent) {
|
|
80
|
+
guard Preferences.shared.dragSnapEnabled else {
|
|
81
|
+
clearTracking()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
guard PermissionChecker.shared.accessibility else {
|
|
85
|
+
clearTracking()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
WorkspaceManager.shared.loadGridConfig()
|
|
90
|
+
modifierModeEnabled = Self.snapModifierPressed()
|
|
91
|
+
windowHasMoved = false
|
|
92
|
+
activeSession = nil
|
|
93
|
+
hideOverlays()
|
|
94
|
+
dragCandidate = captureFocusedWindow(at: NSEvent.mouseLocation)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func handleMouseDragged(_ event: NSEvent) {
|
|
98
|
+
guard Preferences.shared.dragSnapEnabled else {
|
|
99
|
+
clearTracking()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
guard PermissionChecker.shared.accessibility else {
|
|
103
|
+
clearTracking()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
guard let candidate = dragCandidate else { return }
|
|
108
|
+
modifierModeEnabled = Self.snapModifierPressed()
|
|
109
|
+
updateDragProgress(for: candidate)
|
|
110
|
+
updateSnapInteraction(at: NSEvent.mouseLocation)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func handleFlagsChanged(_ event: NSEvent) {
|
|
114
|
+
guard dragCandidate != nil else { return }
|
|
115
|
+
modifierModeEnabled = Self.snapModifierPressed()
|
|
116
|
+
updateSnapInteraction(at: NSEvent.mouseLocation)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private func updateDragProgress(for candidate: DragWindowCandidate) {
|
|
120
|
+
guard let currentFrame = WindowTiler.readAXFrame(candidate.axWindow) else { return }
|
|
121
|
+
|
|
122
|
+
let moved = hypot(
|
|
123
|
+
currentFrame.origin.x - candidate.initialAXFrame.origin.x,
|
|
124
|
+
currentFrame.origin.y - candidate.initialAXFrame.origin.y
|
|
125
|
+
)
|
|
126
|
+
if moved >= 12 {
|
|
127
|
+
windowHasMoved = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func updateSnapInteraction(at mouseLocation: NSPoint) {
|
|
132
|
+
guard windowHasMoved else {
|
|
133
|
+
if activeSession != nil {
|
|
134
|
+
activeSession = nil
|
|
135
|
+
hideOverlays()
|
|
136
|
+
}
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
guard modifierModeEnabled else {
|
|
141
|
+
if activeSession != nil {
|
|
142
|
+
activeSession = nil
|
|
143
|
+
hideOverlays()
|
|
144
|
+
}
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
guard let candidate = dragCandidate else { return }
|
|
149
|
+
if activeSession == nil {
|
|
150
|
+
beginDragSession(with: candidate, mouseLocation: mouseLocation)
|
|
151
|
+
} else {
|
|
152
|
+
updateActiveSession(at: mouseLocation)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func handleMouseUp(_ event: NSEvent) {
|
|
157
|
+
defer { clearTracking() }
|
|
158
|
+
modifierModeEnabled = Self.snapModifierPressed()
|
|
159
|
+
guard modifierModeEnabled, let activeSession else { return }
|
|
160
|
+
|
|
161
|
+
let mouseLocation = NSEvent.mouseLocation
|
|
162
|
+
guard let zone = bestZone(at: mouseLocation, in: activeSession.zones) else { return }
|
|
163
|
+
|
|
164
|
+
DiagnosticLog.shared.info("WindowDragSnap: drop → \(zone.label) (\(zone.id)) on \(zone.screen.localizedName)")
|
|
165
|
+
if let wid = activeSession.wid {
|
|
166
|
+
WindowTiler.tileWindowById(wid: wid, pid: activeSession.pid, to: zone.placement, on: zone.screen)
|
|
167
|
+
WindowTiler.highlightWindowById(wid: wid)
|
|
168
|
+
} else {
|
|
169
|
+
WindowTiler.tileFrontmostViaAX(to: zone.placement)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func beginDragSession(with candidate: DragWindowCandidate, mouseLocation: NSPoint) {
|
|
174
|
+
WorkspaceManager.shared.loadGridConfig()
|
|
175
|
+
let config = WorkspaceManager.shared.snapZonesConfig
|
|
176
|
+
guard config.enabled ?? false else { return }
|
|
177
|
+
|
|
178
|
+
let zones = resolveZones(using: config)
|
|
179
|
+
guard !zones.isEmpty else { return }
|
|
180
|
+
|
|
181
|
+
activeSession = DragSession(
|
|
182
|
+
pid: candidate.pid,
|
|
183
|
+
wid: candidate.wid,
|
|
184
|
+
zones: zones
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
DiagnosticLog.shared.info("WindowDragSnap: tracking drag for pid=\(candidate.pid) wid=\(candidate.wid ?? 0)")
|
|
188
|
+
updateActiveSession(at: mouseLocation)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func updateActiveSession(at mouseLocation: NSPoint) {
|
|
192
|
+
guard let activeSession else { return }
|
|
193
|
+
let hoveredZone = bestZone(at: mouseLocation, in: activeSession.zones)
|
|
194
|
+
render(zones: activeSession.zones, hoveredZone: hoveredZone)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func clearTracking() {
|
|
198
|
+
dragCandidate = nil
|
|
199
|
+
activeSession = nil
|
|
200
|
+
modifierModeEnabled = false
|
|
201
|
+
windowHasMoved = false
|
|
202
|
+
hideOverlays()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func hideOverlays() {
|
|
206
|
+
for panel in overlayPanels.values {
|
|
207
|
+
panel.orderOut(nil)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private func captureFocusedWindow(at mouseLocation: NSPoint) -> DragWindowCandidate? {
|
|
212
|
+
guard let frontApp = NSWorkspace.shared.frontmostApplication,
|
|
213
|
+
frontApp.bundleIdentifier != Bundle.main.bundleIdentifier else {
|
|
214
|
+
return nil
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let appRef = AXUIElementCreateApplication(frontApp.processIdentifier)
|
|
218
|
+
var focusedRef: CFTypeRef?
|
|
219
|
+
guard AXUIElementCopyAttributeValue(appRef, kAXFocusedWindowAttribute as CFString, &focusedRef) == .success,
|
|
220
|
+
let focusedRef else {
|
|
221
|
+
return nil
|
|
222
|
+
}
|
|
223
|
+
let axWindow = focusedRef as! AXUIElement
|
|
224
|
+
guard let axFrame = WindowTiler.readAXFrame(axWindow) else { return nil }
|
|
225
|
+
|
|
226
|
+
let windowRect = Self.screenRect(fromAX: axFrame)
|
|
227
|
+
guard windowRect.insetBy(dx: -8, dy: -8).contains(mouseLocation) else {
|
|
228
|
+
return nil
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
var widValue: CGWindowID = 0
|
|
232
|
+
let wid = _AXUIElementGetWindow(axWindow, &widValue) == .success ? widValue : nil
|
|
233
|
+
|
|
234
|
+
return DragWindowCandidate(
|
|
235
|
+
pid: frontApp.processIdentifier,
|
|
236
|
+
wid: wid,
|
|
237
|
+
axWindow: axWindow,
|
|
238
|
+
initialAXFrame: axFrame
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private func resolveZones(using config: SnapZonesConfig) -> [ResolvedSnapZone] {
|
|
243
|
+
let wm = WorkspaceManager.shared
|
|
244
|
+
let baseZones = (config.rules ?? []).compactMap { zone -> (SnapZoneDefinition, PlacementSpec, (CGFloat, CGFloat, CGFloat, CGFloat), Int)? in
|
|
245
|
+
let placement: PlacementSpec
|
|
246
|
+
switch zone.placement {
|
|
247
|
+
case .named(let name):
|
|
248
|
+
guard let resolved = wm.resolvePlacement(name) else {
|
|
249
|
+
DiagnosticLog.shared.warn("WindowDragSnap: ignoring snap zone \(zone.id) — unknown placement \(name)")
|
|
250
|
+
return nil
|
|
251
|
+
}
|
|
252
|
+
placement = resolved
|
|
253
|
+
case .fractions(let fractionalPlacement):
|
|
254
|
+
placement = .fractions(fractionalPlacement)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let triggerFractions: (CGFloat, CGFloat, CGFloat, CGFloat)
|
|
258
|
+
switch zone.trigger {
|
|
259
|
+
case .named(let name):
|
|
260
|
+
guard let triggerPlacement = wm.resolvePlacement(name) else {
|
|
261
|
+
DiagnosticLog.shared.warn("WindowDragSnap: ignoring snap zone \(zone.id) — unknown trigger \(name)")
|
|
262
|
+
return nil
|
|
263
|
+
}
|
|
264
|
+
triggerFractions = triggerPlacement.fractions
|
|
265
|
+
case .fractions(let placement):
|
|
266
|
+
triggerFractions = placement.fractions
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return (zone, placement, triggerFractions, zone.priority ?? 0)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
var resolved: [ResolvedSnapZone] = []
|
|
273
|
+
for screen in NSScreen.screens {
|
|
274
|
+
let screenID = Self.screenID(for: screen)
|
|
275
|
+
for (zone, placement, triggerFractions, priority) in baseZones {
|
|
276
|
+
let triggerRect = Self.screenRect(for: triggerFractions, on: screen)
|
|
277
|
+
let previewRect = Self.screenRect(fromAX: WindowTiler.tileFrame(for: placement, on: screen))
|
|
278
|
+
let visibleRect = Self.visibleRect(forTriggerRect: triggerRect, previewRect: previewRect, on: screen)
|
|
279
|
+
resolved.append(
|
|
280
|
+
ResolvedSnapZone(
|
|
281
|
+
id: zone.id,
|
|
282
|
+
label: zone.label ?? zone.id,
|
|
283
|
+
placement: placement,
|
|
284
|
+
screen: screen,
|
|
285
|
+
screenID: screenID,
|
|
286
|
+
triggerRect: triggerRect,
|
|
287
|
+
visibleRect: visibleRect,
|
|
288
|
+
previewRect: previewRect,
|
|
289
|
+
priority: priority
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return resolved.sorted {
|
|
296
|
+
if $0.priority != $1.priority {
|
|
297
|
+
return $0.priority > $1.priority
|
|
298
|
+
}
|
|
299
|
+
let leftArea = $0.triggerRect.width * $0.triggerRect.height
|
|
300
|
+
let rightArea = $1.triggerRect.width * $1.triggerRect.height
|
|
301
|
+
if leftArea != rightArea {
|
|
302
|
+
return leftArea < rightArea
|
|
303
|
+
}
|
|
304
|
+
return $0.id < $1.id
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private func bestZone(at mouseLocation: NSPoint, in zones: [ResolvedSnapZone]) -> ResolvedSnapZone? {
|
|
309
|
+
zones.first(where: { $0.triggerRect.contains(mouseLocation) })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private func render(zones: [ResolvedSnapZone], hoveredZone: ResolvedSnapZone?) {
|
|
313
|
+
let config = WorkspaceManager.shared.snapZonesConfig
|
|
314
|
+
let grouped = Dictionary(grouping: zones, by: \.screenID)
|
|
315
|
+
let activeScreenIDs = Set(grouped.keys)
|
|
316
|
+
|
|
317
|
+
for screen in NSScreen.screens {
|
|
318
|
+
let screenID = Self.screenID(for: screen)
|
|
319
|
+
guard let screenZones = grouped[screenID], !screenZones.isEmpty else { continue }
|
|
320
|
+
|
|
321
|
+
let panel = overlayPanels[screenID] ?? makeOverlayPanel(for: screen)
|
|
322
|
+
panel.setFrame(screen.frame, display: false)
|
|
323
|
+
|
|
324
|
+
let localZones = screenZones.map {
|
|
325
|
+
WindowSnapOverlayView.Zone(
|
|
326
|
+
id: $0.id,
|
|
327
|
+
label: $0.label,
|
|
328
|
+
rect: $0.visibleRect.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY),
|
|
329
|
+
isHovered: hoveredZone?.id == $0.id && hoveredZone?.screenID == screenID
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let previewRect = hoveredZone?.screenID == screenID
|
|
334
|
+
? hoveredZone?.previewRect.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY)
|
|
335
|
+
: nil
|
|
336
|
+
|
|
337
|
+
panel.overlayView.model = WindowSnapOverlayView.Model(
|
|
338
|
+
zones: localZones,
|
|
339
|
+
previewRect: previewRect,
|
|
340
|
+
previewLabel: nil,
|
|
341
|
+
zoneOpacity: CGFloat(config.zoneOpacity ?? SnapZonesConfig.defaults.zoneOpacity ?? 0.10),
|
|
342
|
+
highlightOpacity: CGFloat(config.highlightOpacity ?? SnapZonesConfig.defaults.highlightOpacity ?? 0.22),
|
|
343
|
+
previewOpacity: CGFloat(config.previewOpacity ?? SnapZonesConfig.defaults.previewOpacity ?? 0.18),
|
|
344
|
+
cornerRadius: config.cornerRadius ?? SnapZonesConfig.defaults.cornerRadius ?? 18
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
panel.orderFrontRegardless()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (screenID, panel) in overlayPanels where !activeScreenIDs.contains(screenID) {
|
|
351
|
+
panel.orderOut(nil)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private func makeOverlayPanel(for screen: NSScreen) -> WindowSnapOverlayPanel {
|
|
356
|
+
let panel = WindowSnapOverlayPanel(frame: screen.frame)
|
|
357
|
+
overlayPanels[Self.screenID(for: screen)] = panel
|
|
358
|
+
return panel
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private static func screenID(for screen: NSScreen) -> String {
|
|
362
|
+
let key = NSDeviceDescriptionKey("NSScreenNumber")
|
|
363
|
+
if let number = screen.deviceDescription[key] as? NSNumber {
|
|
364
|
+
return number.stringValue
|
|
365
|
+
}
|
|
366
|
+
return screen.localizedName
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private static func screenRect(for fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on screen: NSScreen) -> CGRect {
|
|
370
|
+
let visible = screen.visibleFrame
|
|
371
|
+
let (fx, fy, fw, fh) = fractions
|
|
372
|
+
return CGRect(
|
|
373
|
+
x: visible.minX + visible.width * fx,
|
|
374
|
+
y: visible.maxY - visible.height * (fy + fh),
|
|
375
|
+
width: visible.width * fw,
|
|
376
|
+
height: visible.height * fh
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private static func screenRect(fromAX rect: CGRect) -> CGRect {
|
|
381
|
+
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
382
|
+
return CGRect(
|
|
383
|
+
x: rect.origin.x,
|
|
384
|
+
y: primaryHeight - rect.origin.y - rect.height,
|
|
385
|
+
width: rect.width,
|
|
386
|
+
height: rect.height
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private static func snapModifierPressed() -> Bool {
|
|
391
|
+
let flags = CGEventSource.flagsState(.combinedSessionState)
|
|
392
|
+
return flags.contains(Self.snapModifier().cgEventFlags)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private static func snapModifier() -> SnapModifierKey {
|
|
396
|
+
WorkspaceManager.shared.snapZonesConfig.modifier ?? .command
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static func visibleRect(forTriggerRect triggerRect: CGRect, previewRect: CGRect, on screen: NSScreen) -> CGRect {
|
|
400
|
+
let visible = screen.visibleFrame
|
|
401
|
+
let inset: CGFloat = 18
|
|
402
|
+
let nearLeft = abs(triggerRect.minX - visible.minX) < 8
|
|
403
|
+
let nearRight = abs(triggerRect.maxX - visible.maxX) < 8
|
|
404
|
+
let nearTop = abs(triggerRect.maxY - visible.maxY) < 8
|
|
405
|
+
let nearBottom = abs(triggerRect.minY - visible.minY) < 8
|
|
406
|
+
|
|
407
|
+
if (nearLeft || nearRight) && (nearTop || nearBottom) {
|
|
408
|
+
let width: CGFloat = 94
|
|
409
|
+
let height: CGFloat = 56
|
|
410
|
+
let x = nearLeft ? visible.minX + inset : visible.maxX - inset - width
|
|
411
|
+
let y = nearBottom ? visible.minY + inset : visible.maxY - inset - height
|
|
412
|
+
return CGRect(x: x, y: y, width: width, height: height)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if nearLeft || nearRight {
|
|
416
|
+
let width: CGFloat = 110
|
|
417
|
+
let height: CGFloat = 38
|
|
418
|
+
let x = nearLeft ? visible.minX + inset : visible.maxX - inset - width
|
|
419
|
+
let y = clamp(previewRect.midY - height / 2, min: visible.minY + 54, max: visible.maxY - 54 - height)
|
|
420
|
+
return CGRect(x: x, y: y, width: width, height: height)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if nearTop || nearBottom {
|
|
424
|
+
let width = min(max(triggerRect.width * 0.34, 132), 240)
|
|
425
|
+
let height: CGFloat = 38
|
|
426
|
+
let x = clamp(previewRect.midX - width / 2, min: visible.minX + 54, max: visible.maxX - 54 - width)
|
|
427
|
+
let y = nearBottom ? visible.minY + inset : visible.maxY - inset - height
|
|
428
|
+
return CGRect(x: x, y: y, width: width, height: height)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let width = min(max(previewRect.width * 0.28, 132), 220)
|
|
432
|
+
let height: CGFloat = 38
|
|
433
|
+
let x = clamp(previewRect.midX - width / 2, min: visible.minX + 54, max: visible.maxX - 54 - width)
|
|
434
|
+
let y = clamp(previewRect.maxY - height - 16, min: visible.minY + 40, max: visible.maxY - 40 - height)
|
|
435
|
+
return CGRect(x: x, y: y, width: width, height: height)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private static func clamp(_ value: CGFloat, min lower: CGFloat, max upper: CGFloat) -> CGFloat {
|
|
439
|
+
Swift.max(lower, Swift.min(upper, value))
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private final class WindowSnapOverlayPanel: NSPanel {
|
|
444
|
+
let overlayView = WindowSnapOverlayView(frame: .zero)
|
|
445
|
+
|
|
446
|
+
init(frame: CGRect) {
|
|
447
|
+
super.init(
|
|
448
|
+
contentRect: frame,
|
|
449
|
+
styleMask: [.borderless, .nonactivatingPanel],
|
|
450
|
+
backing: .buffered,
|
|
451
|
+
defer: false
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
isOpaque = false
|
|
455
|
+
backgroundColor = .clear
|
|
456
|
+
hasShadow = false
|
|
457
|
+
ignoresMouseEvents = true
|
|
458
|
+
level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
|
|
459
|
+
collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
|
460
|
+
isMovable = false
|
|
461
|
+
hidesOnDeactivate = false
|
|
462
|
+
animationBehavior = .none
|
|
463
|
+
overlayView.frame = NSRect(origin: .zero, size: frame.size)
|
|
464
|
+
overlayView.autoresizingMask = [.width, .height]
|
|
465
|
+
contentView = overlayView
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
override var canBecomeKey: Bool { false }
|
|
469
|
+
override var canBecomeMain: Bool { false }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private final class WindowSnapOverlayView: NSView {
|
|
473
|
+
struct Zone {
|
|
474
|
+
let id: String
|
|
475
|
+
let label: String
|
|
476
|
+
let rect: CGRect
|
|
477
|
+
let isHovered: Bool
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
struct Model {
|
|
481
|
+
let zones: [Zone]
|
|
482
|
+
let previewRect: CGRect?
|
|
483
|
+
let previewLabel: String?
|
|
484
|
+
let zoneOpacity: CGFloat
|
|
485
|
+
let highlightOpacity: CGFloat
|
|
486
|
+
let previewOpacity: CGFloat
|
|
487
|
+
let cornerRadius: CGFloat
|
|
488
|
+
|
|
489
|
+
static let empty = Model(
|
|
490
|
+
zones: [],
|
|
491
|
+
previewRect: nil,
|
|
492
|
+
previewLabel: nil,
|
|
493
|
+
zoneOpacity: 0.10,
|
|
494
|
+
highlightOpacity: 0.22,
|
|
495
|
+
previewOpacity: 0.18,
|
|
496
|
+
cornerRadius: 18
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var model: Model = .empty {
|
|
501
|
+
didSet { needsDisplay = true }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
override init(frame frameRect: NSRect) {
|
|
505
|
+
super.init(frame: frameRect)
|
|
506
|
+
wantsLayer = true
|
|
507
|
+
layer?.backgroundColor = NSColor.clear.cgColor
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
required init?(coder: NSCoder) {
|
|
511
|
+
fatalError("init(coder:) has not been implemented")
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
515
|
+
NSColor.clear.setFill()
|
|
516
|
+
bounds.fill()
|
|
517
|
+
|
|
518
|
+
for zone in model.zones {
|
|
519
|
+
drawZone(zone)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if let previewRect = model.previewRect {
|
|
523
|
+
drawPreview(previewRect, label: model.previewLabel)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private func drawZone(_ zone: Zone) {
|
|
528
|
+
let rect = zone.rect.insetBy(dx: 1.5, dy: 1.5)
|
|
529
|
+
let radius = min(model.cornerRadius, min(rect.width, rect.height) * 0.34)
|
|
530
|
+
let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
|
|
531
|
+
let idleStrength = max(0.35, min(model.zoneOpacity / 0.10, 1.4))
|
|
532
|
+
let hoverStrength = max(0.35, min(model.highlightOpacity / 0.22, 1.4))
|
|
533
|
+
|
|
534
|
+
let shadow = NSShadow()
|
|
535
|
+
shadow.shadowBlurRadius = zone.isHovered ? 18 : 10
|
|
536
|
+
shadow.shadowOffset = NSSize(width: 0, height: -2)
|
|
537
|
+
shadow.shadowColor = NSColor.black.withAlphaComponent(zone.isHovered ? 0.20 : 0.10)
|
|
538
|
+
|
|
539
|
+
NSGraphicsContext.saveGraphicsState()
|
|
540
|
+
shadow.set()
|
|
541
|
+
let baseTop = NSColor(
|
|
542
|
+
calibratedWhite: 0.13,
|
|
543
|
+
alpha: zone.isHovered ? 0.42 * hoverStrength : 0.22 * idleStrength
|
|
544
|
+
)
|
|
545
|
+
let baseBottom = NSColor(
|
|
546
|
+
calibratedWhite: 0.07,
|
|
547
|
+
alpha: zone.isHovered ? 0.34 * hoverStrength : 0.15 * idleStrength
|
|
548
|
+
)
|
|
549
|
+
NSGradient(starting: baseTop, ending: baseBottom)?.draw(in: path, angle: -90)
|
|
550
|
+
NSGraphicsContext.restoreGraphicsState()
|
|
551
|
+
|
|
552
|
+
if zone.isHovered {
|
|
553
|
+
let glowPath = path.copy() as! NSBezierPath
|
|
554
|
+
glowPath.lineWidth = 6
|
|
555
|
+
NSColor(calibratedRed: 0.25, green: 0.84, blue: 0.58, alpha: model.highlightOpacity * 0.28).setStroke()
|
|
556
|
+
glowPath.stroke()
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
path.lineWidth = zone.isHovered ? 1.6 : 1.0
|
|
560
|
+
NSColor(
|
|
561
|
+
calibratedRed: 0.52,
|
|
562
|
+
green: 0.94,
|
|
563
|
+
blue: 0.72,
|
|
564
|
+
alpha: zone.isHovered ? 0.54 * hoverStrength : 0.10 * idleStrength
|
|
565
|
+
).setStroke()
|
|
566
|
+
path.stroke()
|
|
567
|
+
|
|
568
|
+
let lipRect = CGRect(x: rect.minX + 1.5, y: rect.maxY - 2.5, width: rect.width - 3, height: 2)
|
|
569
|
+
if lipRect.width > 0 {
|
|
570
|
+
let lipPath = NSBezierPath(roundedRect: lipRect, xRadius: 1, yRadius: 1)
|
|
571
|
+
NSColor.white.withAlphaComponent(zone.isHovered ? 0.18 : 0.08).setFill()
|
|
572
|
+
lipPath.fill()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
drawLabel(zone.label, in: rect, emphasized: zone.isHovered)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private func drawPreview(_ rect: CGRect, label: String?) {
|
|
579
|
+
let previewRect = rect.insetBy(dx: 10, dy: 10)
|
|
580
|
+
let radius = min(model.cornerRadius, min(previewRect.width, previewRect.height) * 0.14)
|
|
581
|
+
let path = NSBezierPath(roundedRect: previewRect, xRadius: radius, yRadius: radius)
|
|
582
|
+
|
|
583
|
+
NSColor(calibratedWhite: 1.0, alpha: model.previewOpacity * 0.22).setFill()
|
|
584
|
+
path.fill()
|
|
585
|
+
|
|
586
|
+
path.lineWidth = 1.6
|
|
587
|
+
path.setLineDash([10, 8], count: 2, phase: 0)
|
|
588
|
+
NSColor(
|
|
589
|
+
calibratedRed: 0.44,
|
|
590
|
+
green: 0.90,
|
|
591
|
+
blue: 0.68,
|
|
592
|
+
alpha: max(0.34, model.previewOpacity * 3.2)
|
|
593
|
+
).setStroke()
|
|
594
|
+
path.stroke()
|
|
595
|
+
path.setLineDash([], count: 0, phase: 0)
|
|
596
|
+
|
|
597
|
+
let innerPath = NSBezierPath(roundedRect: previewRect.insetBy(dx: 7, dy: 7), xRadius: max(radius - 4, 8), yRadius: max(radius - 4, 8))
|
|
598
|
+
innerPath.lineWidth = 1
|
|
599
|
+
NSColor.white.withAlphaComponent(max(0.08, model.previewOpacity * 1.2)).setStroke()
|
|
600
|
+
innerPath.stroke()
|
|
601
|
+
|
|
602
|
+
if let label {
|
|
603
|
+
let tagRect = CGRect(x: previewRect.minX + 14, y: previewRect.maxY - 34, width: 110, height: 24)
|
|
604
|
+
let tagPath = NSBezierPath(roundedRect: tagRect, xRadius: 12, yRadius: 12)
|
|
605
|
+
NSColor(calibratedWhite: 0.08, alpha: 0.62).setFill()
|
|
606
|
+
tagPath.fill()
|
|
607
|
+
NSColor.white.withAlphaComponent(0.10).setStroke()
|
|
608
|
+
tagPath.lineWidth = 1
|
|
609
|
+
tagPath.stroke()
|
|
610
|
+
drawLabel(label, in: tagRect, emphasized: true)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private func drawLabel(_ label: String, in rect: CGRect, emphasized: Bool) {
|
|
615
|
+
let font = NSFont.monospacedSystemFont(ofSize: emphasized ? 11 : 10, weight: emphasized ? .semibold : .medium)
|
|
616
|
+
let attributes: [NSAttributedString.Key: Any] = [
|
|
617
|
+
.font: font,
|
|
618
|
+
.foregroundColor: NSColor.white.withAlphaComponent(emphasized ? 0.92 : 0.72),
|
|
619
|
+
]
|
|
620
|
+
let attr = NSAttributedString(string: label.uppercased(), attributes: attributes)
|
|
621
|
+
let size = attr.size()
|
|
622
|
+
let drawPoint = CGPoint(
|
|
623
|
+
x: rect.midX - size.width / 2,
|
|
624
|
+
y: rect.midY - size.height / 2
|
|
625
|
+
)
|
|
626
|
+
attr.draw(at: drawPoint)
|
|
627
|
+
}
|
|
628
|
+
}
|