@lattices/cli 0.4.2 → 0.4.5
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/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +90 -34
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +15 -4
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +351 -191
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/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/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +2 -1
|
@@ -4,6 +4,140 @@ import AppKit
|
|
|
4
4
|
// MARK: - Screen Map View (Standalone)
|
|
5
5
|
|
|
6
6
|
struct ScreenMapView: View {
|
|
7
|
+
private static let canvasPadding: CGFloat = 8
|
|
8
|
+
private static let canvasFitInsets = CGSize(width: 24, height: 16)
|
|
9
|
+
private static let canvasViewportInsets = CGSize(width: 16, height: 16)
|
|
10
|
+
|
|
11
|
+
private struct CanvasMetrics: Equatable {
|
|
12
|
+
let worldBounds: CGRect
|
|
13
|
+
let fitScale: CGFloat
|
|
14
|
+
let effectiveScale: CGFloat
|
|
15
|
+
let mapSize: CGSize
|
|
16
|
+
let centerOffset: CGPoint
|
|
17
|
+
let syncedViewportSize: CGSize
|
|
18
|
+
|
|
19
|
+
init(editor: ScreenMapEditorState?, displays: [DisplayGeometry], viewportSize: CGSize) {
|
|
20
|
+
let fallbackBounds: CGRect = {
|
|
21
|
+
guard let first = displays.first else {
|
|
22
|
+
let size = NSScreen.main?.frame.size ?? CGSize(width: 1920, height: 1080)
|
|
23
|
+
return CGRect(origin: .zero, size: size)
|
|
24
|
+
}
|
|
25
|
+
return displays.dropFirst().reduce(first.cgRect) { $0.union($1.cgRect) }
|
|
26
|
+
}()
|
|
27
|
+
|
|
28
|
+
worldBounds = editor?.canvasWorldBounds ?? fallbackBounds
|
|
29
|
+
|
|
30
|
+
let fitArea = CGSize(
|
|
31
|
+
width: max(viewportSize.width - ScreenMapView.canvasFitInsets.width, 1),
|
|
32
|
+
height: max(viewportSize.height - ScreenMapView.canvasFitInsets.height, 1)
|
|
33
|
+
)
|
|
34
|
+
syncedViewportSize = CGSize(
|
|
35
|
+
width: max(viewportSize.width - ScreenMapView.canvasViewportInsets.width, 1),
|
|
36
|
+
height: max(viewportSize.height - ScreenMapView.canvasViewportInsets.height, 1)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
fitScale = min(
|
|
40
|
+
fitArea.width / max(worldBounds.width, 1),
|
|
41
|
+
fitArea.height / max(worldBounds.height, 1)
|
|
42
|
+
)
|
|
43
|
+
effectiveScale = fitScale * (editor?.zoomLevel ?? 1)
|
|
44
|
+
mapSize = CGSize(width: worldBounds.width * effectiveScale, height: worldBounds.height * effectiveScale)
|
|
45
|
+
centerOffset = CGPoint(
|
|
46
|
+
x: (viewportSize.width - mapSize.width) / 2,
|
|
47
|
+
y: (viewportSize.height - mapSize.height) / 2
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func mapRect(for worldRect: CGRect, minimumSize: CGFloat = 4) -> CGRect {
|
|
52
|
+
CGRect(
|
|
53
|
+
x: (worldRect.origin.x - worldBounds.origin.x) * effectiveScale,
|
|
54
|
+
y: (worldRect.origin.y - worldBounds.origin.y) * effectiveScale,
|
|
55
|
+
width: max(worldRect.width * effectiveScale, minimumSize),
|
|
56
|
+
height: max(worldRect.height * effectiveScale, minimumSize)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private struct CanvasProjection {
|
|
62
|
+
let scale: CGFloat
|
|
63
|
+
let bboxOrigin: CGPoint
|
|
64
|
+
let mapOrigin: CGPoint
|
|
65
|
+
let panOffset: CGPoint
|
|
66
|
+
|
|
67
|
+
init(editor: ScreenMapEditorState) {
|
|
68
|
+
scale = editor.effectiveScale
|
|
69
|
+
bboxOrigin = editor.bboxOrigin
|
|
70
|
+
mapOrigin = editor.mapOrigin
|
|
71
|
+
panOffset = editor.panOffset
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func mapRect(for worldRect: CGRect, minimumSize: CGFloat = 4) -> CGRect {
|
|
75
|
+
CGRect(
|
|
76
|
+
x: (worldRect.origin.x - bboxOrigin.x) * scale,
|
|
77
|
+
y: (worldRect.origin.y - bboxOrigin.y) * scale,
|
|
78
|
+
width: max(worldRect.width * scale, minimumSize),
|
|
79
|
+
height: max(worldRect.height * scale, minimumSize)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func mapPoint(forCanvasPoint canvasPoint: CGPoint) -> CGPoint {
|
|
84
|
+
CGPoint(
|
|
85
|
+
x: canvasPoint.x - ScreenMapView.canvasPadding - mapOrigin.x - panOffset.x,
|
|
86
|
+
y: canvasPoint.y - ScreenMapView.canvasPadding - mapOrigin.y - panOffset.y
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private struct CanvasHit {
|
|
92
|
+
let id: UInt32
|
|
93
|
+
let mapRect: CGRect
|
|
94
|
+
let mapPoint: CGPoint
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private struct CanvasSyncKey: Equatable {
|
|
98
|
+
let viewportSize: CGSize
|
|
99
|
+
let worldBounds: CGRect
|
|
100
|
+
let zoomLevel: CGFloat
|
|
101
|
+
let navigationRevision: Int
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private struct MiniMapMetrics {
|
|
105
|
+
let worldBounds: CGRect
|
|
106
|
+
let scale: CGFloat
|
|
107
|
+
let drawSize: CGSize
|
|
108
|
+
let offset: CGPoint
|
|
109
|
+
|
|
110
|
+
init(worldBounds: CGRect, canvasSize: CGSize) {
|
|
111
|
+
self.worldBounds = worldBounds
|
|
112
|
+
let scaleW = canvasSize.width / max(worldBounds.width, 1)
|
|
113
|
+
let scaleH = canvasSize.height / max(worldBounds.height, 1)
|
|
114
|
+
scale = min(scaleW, scaleH)
|
|
115
|
+
drawSize = CGSize(width: worldBounds.width * scale, height: worldBounds.height * scale)
|
|
116
|
+
offset = CGPoint(
|
|
117
|
+
x: (canvasSize.width - drawSize.width) / 2,
|
|
118
|
+
y: (canvasSize.height - drawSize.height) / 2
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func rect(for worldRect: CGRect, minimumSize: CGFloat) -> CGRect {
|
|
123
|
+
CGRect(
|
|
124
|
+
x: (worldRect.origin.x - worldBounds.origin.x) * scale + offset.x,
|
|
125
|
+
y: (worldRect.origin.y - worldBounds.origin.y) * scale + offset.y,
|
|
126
|
+
width: max(worldRect.width * scale, minimumSize),
|
|
127
|
+
height: max(worldRect.height * scale, minimumSize)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func worldPoint(for localPoint: CGPoint) -> CGPoint {
|
|
132
|
+
let localX = min(max(localPoint.x - offset.x, 0), drawSize.width)
|
|
133
|
+
let localY = min(max(localPoint.y - offset.y, 0), drawSize.height)
|
|
134
|
+
return CGPoint(
|
|
135
|
+
x: worldBounds.origin.x + localX / max(scale, 0.0001),
|
|
136
|
+
y: worldBounds.origin.y + localY / max(scale, 0.0001)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
7
141
|
@ObservedObject var controller: ScreenMapController
|
|
8
142
|
var onNavigate: ((AppPage) -> Void)? = nil
|
|
9
143
|
@ObservedObject private var daemon = DaemonServer.shared
|
|
@@ -18,7 +152,6 @@ struct ScreenMapView: View {
|
|
|
18
152
|
@State private var scrollWheelMonitor: Any?
|
|
19
153
|
@State private var screenMapCanvasOrigin: CGPoint = .zero
|
|
20
154
|
@State private var screenMapCanvasSize: CGSize = .zero
|
|
21
|
-
@State private var screenMapTitleBarHeight: CGFloat = 0 // reserved for coordinate math
|
|
22
155
|
@State private var screenMapClickWindowId: UInt32? = nil
|
|
23
156
|
@State private var screenMapClickPoint: NSPoint = .zero
|
|
24
157
|
@State private var hoveredWindowId: UInt32?
|
|
@@ -28,6 +161,9 @@ struct ScreenMapView: View {
|
|
|
28
161
|
@State private var sidebarDragWindowId: UInt32? = nil
|
|
29
162
|
@State private var sidebarDragOffset: CGSize = .zero
|
|
30
163
|
@State private var expandedLayers: Set<Int> = []
|
|
164
|
+
@State private var showUnnamedLayers: Bool = false
|
|
165
|
+
@State private var showSets: Bool = false
|
|
166
|
+
@State private var showExplorer: Bool = false
|
|
31
167
|
@State private var mouseMovedMonitor: Any?
|
|
32
168
|
@State private var sidebarWidth: CGFloat = 180
|
|
33
169
|
@State private var isDraggingSidebar: Bool = false
|
|
@@ -40,7 +176,6 @@ struct ScreenMapView: View {
|
|
|
40
176
|
@State private var isSpaceHeld: Bool = false
|
|
41
177
|
@State private var spaceDragStart: NSPoint? = nil
|
|
42
178
|
@State private var spaceDragPanStart: CGPoint = .zero
|
|
43
|
-
@State private var flagsMonitor: Any?
|
|
44
179
|
@State private var searchOverlayFrame: CGRect = .zero
|
|
45
180
|
|
|
46
181
|
var body: some View {
|
|
@@ -73,17 +208,7 @@ struct ScreenMapView: View {
|
|
|
73
208
|
if controller.isSearchActive, let editor = controller.editor {
|
|
74
209
|
floatingSearchOverlay(editor: editor)
|
|
75
210
|
}
|
|
76
|
-
// Viewport controls —
|
|
77
|
-
if let editor = controller.editor {
|
|
78
|
-
VStack {
|
|
79
|
-
Spacer()
|
|
80
|
-
HStack {
|
|
81
|
-
Spacer()
|
|
82
|
-
canvasViewportDock(editor: editor)
|
|
83
|
-
.padding(10)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
211
|
+
// Viewport controls removed — accessible via keyboard shortcuts
|
|
87
212
|
}
|
|
88
213
|
if let editor = controller.editor {
|
|
89
214
|
panelResizeHandle(isActive: $isDraggingInspector, width: $inspectorWidth,
|
|
@@ -117,10 +242,7 @@ struct ScreenMapView: View {
|
|
|
117
242
|
private func displayToolbar(editor: ScreenMapEditorState) -> some View {
|
|
118
243
|
HStack(spacing: 4) {
|
|
119
244
|
Button {
|
|
120
|
-
|
|
121
|
-
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
122
|
-
controller.flash(editor.focusedDisplay?.label ?? "All displays")
|
|
123
|
-
controller.objectWillChange.send()
|
|
245
|
+
controller.stepDisplayFocus(.previous)
|
|
124
246
|
} label: {
|
|
125
247
|
Image(systemName: "chevron.left")
|
|
126
248
|
.font(.system(size: 8, weight: .semibold))
|
|
@@ -131,9 +253,7 @@ struct ScreenMapView: View {
|
|
|
131
253
|
.buttonStyle(.plain)
|
|
132
254
|
|
|
133
255
|
Button {
|
|
134
|
-
|
|
135
|
-
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
136
|
-
controller.objectWillChange.send()
|
|
256
|
+
controller.setDisplayFocus(nil)
|
|
137
257
|
} label: {
|
|
138
258
|
displayToolbarPill(name: "All", isActive: editor.focusedDisplayIndex == nil)
|
|
139
259
|
}
|
|
@@ -142,9 +262,7 @@ struct ScreenMapView: View {
|
|
|
142
262
|
ForEach(Array(editor.spatialDisplayOrder.enumerated()), id: \.element.index) { spatialPos, disp in
|
|
143
263
|
let isActive = editor.focusedDisplayIndex == disp.index
|
|
144
264
|
Button {
|
|
145
|
-
|
|
146
|
-
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
147
|
-
controller.objectWillChange.send()
|
|
265
|
+
controller.setDisplayFocus(disp.index)
|
|
148
266
|
} label: {
|
|
149
267
|
displayToolbarPill(
|
|
150
268
|
badge: spatialPos + 1,
|
|
@@ -156,10 +274,7 @@ struct ScreenMapView: View {
|
|
|
156
274
|
}
|
|
157
275
|
|
|
158
276
|
Button {
|
|
159
|
-
|
|
160
|
-
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
161
|
-
controller.flash(editor.focusedDisplay?.label ?? "All displays")
|
|
162
|
-
controller.objectWillChange.send()
|
|
277
|
+
controller.stepDisplayFocus(.next)
|
|
163
278
|
} label: {
|
|
164
279
|
Image(systemName: "chevron.right")
|
|
165
280
|
.font(.system(size: 8, weight: .semibold))
|
|
@@ -1316,8 +1431,6 @@ struct ScreenMapView: View {
|
|
|
1316
1431
|
// MARK: - Layer Sidebar
|
|
1317
1432
|
|
|
1318
1433
|
private func layerSidebar(editor: ScreenMapEditorState) -> some View {
|
|
1319
|
-
let layers = editor.effectiveLayers
|
|
1320
|
-
|
|
1321
1434
|
return VStack(spacing: 0) {
|
|
1322
1435
|
// Header
|
|
1323
1436
|
HStack {
|
|
@@ -1337,146 +1450,61 @@ struct ScreenMapView: View {
|
|
|
1337
1450
|
}
|
|
1338
1451
|
.padding(.bottom, 8)
|
|
1339
1452
|
|
|
1340
|
-
//
|
|
1453
|
+
// Layer list
|
|
1341
1454
|
ScrollView(.vertical, showsIndicators: false) {
|
|
1455
|
+
let namedLayers = editor.namedEffectiveLayers
|
|
1456
|
+
let unnamedLayers = editor.unnamedEffectiveLayers
|
|
1457
|
+
|
|
1342
1458
|
VStack(spacing: 2) {
|
|
1343
1459
|
layerTreeHeader(
|
|
1344
1460
|
label: "All",
|
|
1345
|
-
count: editor.
|
|
1346
|
-
? editor.windows.filter { $0.displayIndex == editor.focusedDisplayIndex! }.count
|
|
1347
|
-
: editor.windows.count,
|
|
1461
|
+
count: editor.scopedWindowCount,
|
|
1348
1462
|
isActive: editor.isShowingAll,
|
|
1349
1463
|
color: Palette.running
|
|
1350
1464
|
) {
|
|
1351
1465
|
editor.selectLayer(nil)
|
|
1352
|
-
controller.objectWillChange.send()
|
|
1353
1466
|
}
|
|
1354
1467
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
let fullName = editor.layerNames[layer]
|
|
1359
|
-
let color = Self.layerColor(for: layer)
|
|
1360
|
-
let isActive = editor.isLayerSelected(layer)
|
|
1361
|
-
let isDropTarget = dropTargetLayer == layer
|
|
1362
|
-
let layerWindows = layerWindowsForTree(editor: editor, layer: layer)
|
|
1363
|
-
|
|
1364
|
-
VStack(spacing: 0) {
|
|
1365
|
-
layerTreeHeader(label: fullName ?? displayName,
|
|
1366
|
-
count: layerWindows.count,
|
|
1367
|
-
isActive: isActive,
|
|
1368
|
-
color: color,
|
|
1369
|
-
isExpandable: true,
|
|
1370
|
-
isExpanded: expandedLayers.contains(layer),
|
|
1371
|
-
onToggleExpand: {
|
|
1372
|
-
if expandedLayers.contains(layer) {
|
|
1373
|
-
expandedLayers.remove(layer)
|
|
1374
|
-
} else {
|
|
1375
|
-
expandedLayers.insert(layer)
|
|
1376
|
-
}
|
|
1377
|
-
}) {
|
|
1378
|
-
if NSEvent.modifierFlags.contains(.command) {
|
|
1379
|
-
editor.toggleLayerSelection(layer)
|
|
1380
|
-
} else {
|
|
1381
|
-
editor.selectLayer(layer)
|
|
1382
|
-
}
|
|
1383
|
-
// Auto-expand on selection
|
|
1384
|
-
expandedLayers.insert(layer)
|
|
1385
|
-
controller.objectWillChange.send()
|
|
1386
|
-
}
|
|
1468
|
+
ForEach(namedLayers, id: \.self) { layer in
|
|
1469
|
+
layerRow(layer: layer, editor: editor)
|
|
1470
|
+
}
|
|
1387
1471
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
.foregroundColor(isSelected ? Palette.running : Palette.textDim)
|
|
1402
|
-
.lineLimit(1)
|
|
1403
|
-
Spacer()
|
|
1404
|
-
if win.hasEdits {
|
|
1405
|
-
Circle()
|
|
1406
|
-
.fill(Color.orange)
|
|
1407
|
-
.frame(width: 4, height: 4)
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
.padding(.vertical, 2)
|
|
1411
|
-
.padding(.horizontal, 4)
|
|
1412
|
-
.background(
|
|
1413
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1414
|
-
.fill(isSelected ? Palette.running.opacity(0.08) : Color.clear)
|
|
1415
|
-
)
|
|
1416
|
-
.contentShape(Rectangle())
|
|
1417
|
-
.opacity(isDragging ? 0.4 : 1.0)
|
|
1418
|
-
.offset(isDragging ? sidebarDragOffset : .zero)
|
|
1419
|
-
.zIndex(isDragging ? 10 : 0)
|
|
1420
|
-
.gesture(
|
|
1421
|
-
DragGesture(minimumDistance: 4, coordinateSpace: .named("layerSidebar"))
|
|
1422
|
-
.onChanged { value in
|
|
1423
|
-
sidebarDragWindowId = win.id
|
|
1424
|
-
sidebarDragOffset = value.translation
|
|
1425
|
-
controller.selectSingle(win.id)
|
|
1426
|
-
// Hit-test layer rows
|
|
1427
|
-
let pt = value.location
|
|
1428
|
-
var hit: Int? = nil
|
|
1429
|
-
for (l, frame) in layerRowFrames {
|
|
1430
|
-
if l != layer && frame.contains(pt) {
|
|
1431
|
-
hit = l
|
|
1432
|
-
break
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
dropTargetLayer = hit
|
|
1436
|
-
}
|
|
1437
|
-
.onEnded { _ in
|
|
1438
|
-
if let targetLayer = dropTargetLayer {
|
|
1439
|
-
editor.reassignLayer(windowId: win.id, toLayer: targetLayer, fitToAvailable: true)
|
|
1440
|
-
controller.flash("Moved to L\(targetLayer)")
|
|
1441
|
-
controller.objectWillChange.send()
|
|
1442
|
-
}
|
|
1443
|
-
sidebarDragWindowId = nil
|
|
1444
|
-
sidebarDragOffset = .zero
|
|
1445
|
-
dropTargetLayer = nil
|
|
1446
|
-
}
|
|
1447
|
-
)
|
|
1448
|
-
.onTapGesture {
|
|
1449
|
-
if NSEvent.modifierFlags.contains(.command) {
|
|
1450
|
-
controller.toggleSelection(win.id)
|
|
1451
|
-
} else {
|
|
1452
|
-
controller.selectSingle(win.id)
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
.padding(.leading, 4)
|
|
1458
|
-
.padding(.top, 2)
|
|
1459
|
-
}
|
|
1472
|
+
if !unnamedLayers.isEmpty {
|
|
1473
|
+
HStack(spacing: 4) {
|
|
1474
|
+
let totalWindows = unnamedLayers.reduce(0) { $0 + editor.layerTreeWindows(for: $1).count }
|
|
1475
|
+
Image(systemName: showUnnamedLayers ? "chevron.down" : "chevron.right")
|
|
1476
|
+
.font(.system(size: 6, weight: .bold))
|
|
1477
|
+
.foregroundColor(Palette.textMuted)
|
|
1478
|
+
Text("\(unnamedLayers.count) more")
|
|
1479
|
+
.font(Typo.mono(8))
|
|
1480
|
+
.foregroundColor(Palette.textMuted)
|
|
1481
|
+
Text("· \(totalWindows)w")
|
|
1482
|
+
.font(Typo.mono(7))
|
|
1483
|
+
.foregroundColor(Palette.textDim)
|
|
1484
|
+
Spacer()
|
|
1460
1485
|
}
|
|
1461
|
-
.
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
)
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1486
|
+
.padding(.vertical, 4)
|
|
1487
|
+
.padding(.horizontal, 4)
|
|
1488
|
+
.contentShape(Rectangle())
|
|
1489
|
+
.simultaneousGesture(TapGesture().onEnded { showUnnamedLayers.toggle() })
|
|
1490
|
+
|
|
1491
|
+
if showUnnamedLayers {
|
|
1492
|
+
ForEach(unnamedLayers, id: \.self) { layer in
|
|
1493
|
+
layerRow(layer: layer, editor: editor)
|
|
1469
1494
|
}
|
|
1470
|
-
|
|
1495
|
+
}
|
|
1471
1496
|
}
|
|
1472
1497
|
}
|
|
1473
1498
|
}
|
|
1474
1499
|
.coordinateSpace(name: "layerSidebar")
|
|
1475
1500
|
|
|
1476
1501
|
Spacer(minLength: 8)
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1502
|
+
collapsibleSection(title: "SETS", count: controller.windowSets.count, isExpanded: $showSets) {
|
|
1503
|
+
windowSetsSection(editor: editor)
|
|
1504
|
+
}
|
|
1505
|
+
collapsibleSection(title: "EXPLORER", count: editor.canvasExplorerRegions.count, isExpanded: $showExplorer) {
|
|
1506
|
+
canvasExplorer(editor: editor)
|
|
1507
|
+
}
|
|
1480
1508
|
Spacer(minLength: 8)
|
|
1481
1509
|
sidebarMiniMap(editor: editor)
|
|
1482
1510
|
}
|
|
@@ -1486,68 +1514,192 @@ struct ScreenMapView: View {
|
|
|
1486
1514
|
.onPreferenceChange(LayerRowFrameKey.self) { layerRowFrames = $0 }
|
|
1487
1515
|
}
|
|
1488
1516
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1517
|
+
@ViewBuilder
|
|
1518
|
+
private func layerRow(layer: Int, editor: ScreenMapEditorState) -> some View {
|
|
1519
|
+
let displayName = editor.layerDisplayName(for: layer)
|
|
1520
|
+
let fullName = editor.layerNames[layer]
|
|
1521
|
+
let color = Self.layerColor(for: layer)
|
|
1522
|
+
let isActive = editor.isLayerSelected(layer)
|
|
1523
|
+
let isDropTarget = dropTargetLayer == layer
|
|
1524
|
+
let layerWindows = editor.layerTreeWindows(for: layer)
|
|
1525
|
+
|
|
1526
|
+
VStack(spacing: 0) {
|
|
1527
|
+
layerTreeHeader(label: fullName ?? displayName,
|
|
1528
|
+
count: layerWindows.count,
|
|
1529
|
+
isActive: isActive,
|
|
1530
|
+
color: color,
|
|
1531
|
+
isExpandable: true,
|
|
1532
|
+
isExpanded: expandedLayers.contains(layer),
|
|
1533
|
+
onToggleExpand: { toggleExpandedLayer(layer) }) {
|
|
1534
|
+
selectSidebarLayer(layer, editor: editor)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if expandedLayers.contains(layer) {
|
|
1538
|
+
VStack(spacing: 0) {
|
|
1539
|
+
ForEach(layerWindows) { win in
|
|
1540
|
+
let isSelected = controller.selectedWindowIds.contains(win.id)
|
|
1541
|
+
let isDragging = sidebarDragWindowId == win.id
|
|
1542
|
+
HStack(spacing: 4) {
|
|
1543
|
+
Rectangle()
|
|
1544
|
+
.fill(color.opacity(0.4))
|
|
1545
|
+
.frame(width: 1, height: 12)
|
|
1546
|
+
.padding(.leading, 8)
|
|
1547
|
+
Text(win.app)
|
|
1548
|
+
.font(Typo.mono(8))
|
|
1549
|
+
.foregroundColor(isSelected ? Palette.running : Palette.textDim)
|
|
1550
|
+
.lineLimit(1)
|
|
1551
|
+
Spacer()
|
|
1552
|
+
if win.hasEdits {
|
|
1553
|
+
Circle()
|
|
1554
|
+
.fill(Color.orange)
|
|
1555
|
+
.frame(width: 4, height: 4)
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
.padding(.vertical, 2)
|
|
1559
|
+
.padding(.horizontal, 4)
|
|
1560
|
+
.background(
|
|
1561
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1562
|
+
.fill(isSelected ? Palette.running.opacity(0.08) : Color.clear)
|
|
1563
|
+
)
|
|
1564
|
+
.contentShape(Rectangle())
|
|
1565
|
+
.opacity(isDragging ? 0.4 : 1.0)
|
|
1566
|
+
.offset(isDragging ? sidebarDragOffset : .zero)
|
|
1567
|
+
.zIndex(isDragging ? 10 : 0)
|
|
1568
|
+
.gesture(
|
|
1569
|
+
DragGesture(minimumDistance: 4, coordinateSpace: .named("layerSidebar"))
|
|
1570
|
+
.onChanged { value in
|
|
1571
|
+
handleSidebarWindowDragChanged(value, sourceLayer: layer, windowId: win.id)
|
|
1572
|
+
}
|
|
1573
|
+
.onEnded { _ in
|
|
1574
|
+
finishSidebarWindowDrag(win, editor: editor)
|
|
1575
|
+
}
|
|
1576
|
+
)
|
|
1577
|
+
.simultaneousGesture(TapGesture().onEnded {
|
|
1578
|
+
selectSidebarWindow(win.id)
|
|
1579
|
+
})
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
.padding(.leading, 4)
|
|
1583
|
+
.padding(.top, 2)
|
|
1584
|
+
}
|
|
1493
1585
|
}
|
|
1494
|
-
|
|
1586
|
+
.overlay(
|
|
1587
|
+
RoundedRectangle(cornerRadius: 4)
|
|
1588
|
+
.strokeBorder(isDropTarget ? Palette.running : Color.clear, lineWidth: 1.5)
|
|
1589
|
+
)
|
|
1590
|
+
.background(
|
|
1591
|
+
GeometryReader { geo in
|
|
1592
|
+
Color.clear.preference(key: LayerRowFrameKey.self,
|
|
1593
|
+
value: [layer: geo.frame(in: .named("layerSidebar"))])
|
|
1594
|
+
}
|
|
1595
|
+
)
|
|
1495
1596
|
}
|
|
1496
1597
|
|
|
1497
|
-
private func
|
|
1498
|
-
|
|
1499
|
-
|
|
1598
|
+
private func toggleExpandedLayer(_ layer: Int) {
|
|
1599
|
+
if expandedLayers.contains(layer) {
|
|
1600
|
+
expandedLayers.remove(layer)
|
|
1601
|
+
} else {
|
|
1602
|
+
expandedLayers.insert(layer)
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1500
1605
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1606
|
+
private func selectSidebarLayer(_ layer: Int, editor: ScreenMapEditorState) {
|
|
1607
|
+
if NSEvent.modifierFlags.contains(.command) {
|
|
1608
|
+
editor.toggleLayerSelection(layer)
|
|
1609
|
+
} else {
|
|
1610
|
+
editor.selectLayer(layer)
|
|
1611
|
+
}
|
|
1612
|
+
expandedLayers.insert(layer)
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
private func selectSidebarWindow(_ windowId: UInt32) {
|
|
1616
|
+
if NSEvent.modifierFlags.contains(.command) {
|
|
1617
|
+
controller.toggleSelection(windowId)
|
|
1618
|
+
} else {
|
|
1619
|
+
controller.selectSingle(windowId)
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
private func resolveSidebarDropTarget(at point: CGPoint, excluding layer: Int) -> Int? {
|
|
1624
|
+
for (candidate, frame) in layerRowFrames where candidate != layer {
|
|
1625
|
+
if frame.contains(point) {
|
|
1626
|
+
return candidate
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return nil
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private func handleSidebarWindowDragChanged(_ value: DragGesture.Value, sourceLayer: Int, windowId: UInt32) {
|
|
1633
|
+
sidebarDragWindowId = windowId
|
|
1634
|
+
sidebarDragOffset = value.translation
|
|
1635
|
+
controller.selectSingle(windowId)
|
|
1636
|
+
dropTargetLayer = resolveSidebarDropTarget(at: value.location, excluding: sourceLayer)
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
private func finishSidebarWindowDrag(_ win: ScreenMapWindowEntry, editor: ScreenMapEditorState) {
|
|
1640
|
+
if let targetLayer = dropTargetLayer {
|
|
1641
|
+
editor.reassignLayer(windowId: win.id, toLayer: targetLayer, fitToAvailable: true)
|
|
1642
|
+
controller.flash("Moved to L\(targetLayer)")
|
|
1643
|
+
}
|
|
1644
|
+
sidebarDragWindowId = nil
|
|
1645
|
+
sidebarDragOffset = .zero
|
|
1646
|
+
dropTargetLayer = nil
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
private func collapsibleSection<Content: View>(title: String, count: Int, isExpanded: Binding<Bool>,
|
|
1650
|
+
@ViewBuilder content: @escaping () -> Content) -> some View {
|
|
1651
|
+
VStack(spacing: 0) {
|
|
1652
|
+
HStack(spacing: 4) {
|
|
1653
|
+
Image(systemName: isExpanded.wrappedValue ? "chevron.down" : "chevron.right")
|
|
1654
|
+
.font(.system(size: 6, weight: .bold))
|
|
1655
|
+
.foregroundColor(Palette.textMuted)
|
|
1656
|
+
Text(title)
|
|
1504
1657
|
.font(Typo.monoBold(8))
|
|
1505
1658
|
.foregroundColor(Palette.textMuted)
|
|
1506
|
-
Text("\(
|
|
1659
|
+
Text("\(count)")
|
|
1507
1660
|
.font(Typo.mono(7))
|
|
1508
1661
|
.foregroundColor(Palette.textDim)
|
|
1509
1662
|
Spacer()
|
|
1510
|
-
Button {
|
|
1511
|
-
controller.createWindowSetFromSelection()
|
|
1512
|
-
} label: {
|
|
1513
|
-
Text("u save")
|
|
1514
|
-
.font(Typo.monoBold(7))
|
|
1515
|
-
.foregroundColor(canSave ? Self.shelfGreen : Palette.textMuted)
|
|
1516
|
-
.padding(.horizontal, 5)
|
|
1517
|
-
.padding(.vertical, 3)
|
|
1518
|
-
.background(
|
|
1519
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1520
|
-
.fill(canSave ? Self.shelfGreen.opacity(0.12) : Palette.surface.opacity(0.7))
|
|
1521
|
-
.overlay(
|
|
1522
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1523
|
-
.strokeBorder(canSave ? Self.shelfGreen.opacity(0.25) : Palette.border, lineWidth: 0.5)
|
|
1524
|
-
)
|
|
1525
|
-
)
|
|
1526
|
-
}
|
|
1527
|
-
.buttonStyle(.plain)
|
|
1528
|
-
.disabled(!canSave)
|
|
1529
1663
|
}
|
|
1664
|
+
.padding(.vertical, 4)
|
|
1665
|
+
.contentShape(Rectangle())
|
|
1666
|
+
.simultaneousGesture(TapGesture().onEnded { isExpanded.wrappedValue.toggle() })
|
|
1530
1667
|
|
|
1668
|
+
if isExpanded.wrappedValue {
|
|
1669
|
+
content()
|
|
1670
|
+
.padding(.top, 4)
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
.padding(.bottom, 4)
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
private func windowSetsSection(editor: ScreenMapEditorState) -> some View {
|
|
1677
|
+
let sets = controller.windowSets
|
|
1678
|
+
let canSave = !controller.selectedWindowIds.isEmpty
|
|
1679
|
+
|
|
1680
|
+
return VStack(alignment: .leading, spacing: 4) {
|
|
1531
1681
|
if sets.isEmpty {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1682
|
+
HStack {
|
|
1683
|
+
Text("No sets yet.")
|
|
1684
|
+
.font(Typo.mono(7))
|
|
1685
|
+
.foregroundColor(Palette.textMuted)
|
|
1686
|
+
Spacer()
|
|
1687
|
+
Button {
|
|
1688
|
+
controller.createWindowSetFromSelection()
|
|
1689
|
+
} label: {
|
|
1690
|
+
Text("u save")
|
|
1691
|
+
.font(Typo.monoBold(7))
|
|
1692
|
+
.foregroundColor(canSave ? Self.shelfGreen : Palette.textMuted)
|
|
1693
|
+
}
|
|
1694
|
+
.buttonStyle(.plain)
|
|
1695
|
+
.disabled(!canSave)
|
|
1696
|
+
}
|
|
1536
1697
|
} else {
|
|
1537
1698
|
ForEach(sets) { set in
|
|
1538
1699
|
windowSetRow(set: set, editor: editor)
|
|
1539
1700
|
}
|
|
1540
1701
|
}
|
|
1541
1702
|
}
|
|
1542
|
-
.padding(6)
|
|
1543
|
-
.background(
|
|
1544
|
-
RoundedRectangle(cornerRadius: 6)
|
|
1545
|
-
.fill(Color.black.opacity(0.4))
|
|
1546
|
-
.overlay(
|
|
1547
|
-
RoundedRectangle(cornerRadius: 6)
|
|
1548
|
-
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
1549
|
-
)
|
|
1550
|
-
)
|
|
1551
1703
|
}
|
|
1552
1704
|
|
|
1553
1705
|
private func windowSetRow(set: ScreenMapWindowSet, editor: ScreenMapEditorState) -> some View {
|
|
@@ -1642,118 +1794,60 @@ struct ScreenMapView: View {
|
|
|
1642
1794
|
.fill(isActive ? color.opacity(0.12) : Color.clear)
|
|
1643
1795
|
)
|
|
1644
1796
|
.contentShape(Rectangle())
|
|
1645
|
-
.
|
|
1797
|
+
.simultaneousGesture(TapGesture().onEnded { action() })
|
|
1646
1798
|
}
|
|
1647
1799
|
|
|
1648
1800
|
// MARK: - Canvas
|
|
1649
1801
|
|
|
1650
1802
|
private func screenMapCanvas(editor: ScreenMapEditorState?) -> some View {
|
|
1651
1803
|
let isFocused = editor?.focusedDisplayIndex != nil
|
|
1652
|
-
let
|
|
1804
|
+
let canvasWindows = editor?.renderedCanvasWindows ?? []
|
|
1653
1805
|
let displays = editor?.displays ?? []
|
|
1654
1806
|
let zoomLevel = editor?.zoomLevel ?? 1.0
|
|
1655
1807
|
let panOffset = editor?.panOffset ?? .zero
|
|
1656
1808
|
|
|
1657
1809
|
return GeometryReader { geo in
|
|
1658
|
-
let
|
|
1659
|
-
let
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1666
|
-
guard !displays.isEmpty else {
|
|
1667
|
-
let s = NSScreen.main?.frame ?? CGRect(x: 0, y: 0, width: 1920, height: 1080)
|
|
1668
|
-
return CGRect(origin: .zero, size: s.size)
|
|
1669
|
-
}
|
|
1670
|
-
var union = displays[0].cgRect
|
|
1671
|
-
for d in displays.dropFirst() { union = union.union(d.cgRect) }
|
|
1672
|
-
return union.insetBy(dx: -bboxPad, dy: -bboxPad)
|
|
1673
|
-
}()
|
|
1674
|
-
let bboxOriginPt = bbox.origin
|
|
1675
|
-
let screenW = bbox.width
|
|
1676
|
-
let screenH = bbox.height
|
|
1677
|
-
|
|
1678
|
-
let fitScale = min(availW / screenW, availH / screenH)
|
|
1679
|
-
let effScale = fitScale * zoomLevel
|
|
1680
|
-
let mapW = screenW * effScale
|
|
1681
|
-
let mapH = screenH * effScale
|
|
1682
|
-
let centerX = (geo.size.width - mapW) / 2
|
|
1683
|
-
let centerY = (geo.size.height - mapH) / 2
|
|
1810
|
+
let metrics = CanvasMetrics(editor: editor, displays: displays, viewportSize: geo.size)
|
|
1811
|
+
let syncKey = CanvasSyncKey(
|
|
1812
|
+
viewportSize: geo.size,
|
|
1813
|
+
worldBounds: metrics.worldBounds,
|
|
1814
|
+
zoomLevel: zoomLevel,
|
|
1815
|
+
navigationRevision: editor?.canvasNavigationRevision ?? 0
|
|
1816
|
+
)
|
|
1684
1817
|
|
|
1685
1818
|
ZStack(alignment: .topLeading) {
|
|
1686
1819
|
// Per-display background rectangles
|
|
1687
|
-
if isFocused,
|
|
1688
|
-
focusedDisplayBackground(
|
|
1820
|
+
if isFocused, editor?.focusedDisplay != nil {
|
|
1821
|
+
focusedDisplayBackground(mapSize: metrics.mapSize)
|
|
1689
1822
|
} else if displays.count > 1 {
|
|
1690
|
-
multiDisplayBackgrounds(displays: displays, editor: editor,
|
|
1823
|
+
multiDisplayBackgrounds(displays: displays, editor: editor, metrics: metrics)
|
|
1691
1824
|
} else {
|
|
1692
|
-
singleDisplayBackground(
|
|
1825
|
+
singleDisplayBackground(mapSize: metrics.mapSize)
|
|
1693
1826
|
}
|
|
1694
1827
|
|
|
1695
1828
|
// Ghost outlines for edited windows
|
|
1696
|
-
ForEach(
|
|
1697
|
-
let
|
|
1698
|
-
let x = (f.origin.x - bboxOriginPt.x) * effScale
|
|
1699
|
-
let y = (f.origin.y - bboxOriginPt.y) * effScale
|
|
1700
|
-
let w = max(f.width * effScale, 4)
|
|
1701
|
-
let h = max(f.height * effScale, 4)
|
|
1829
|
+
ForEach(canvasWindows.filter(\.hasEdits)) { win in
|
|
1830
|
+
let rect = metrics.mapRect(for: win.originalFrame)
|
|
1702
1831
|
|
|
1703
1832
|
RoundedRectangle(cornerRadius: 2)
|
|
1704
1833
|
.strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [4, 3]))
|
|
1705
1834
|
.foregroundColor(Palette.textMuted.opacity(0.4))
|
|
1706
|
-
.frame(width:
|
|
1707
|
-
.offset(x:
|
|
1835
|
+
.frame(width: rect.width, height: rect.height)
|
|
1836
|
+
.offset(x: rect.minX, y: rect.minY)
|
|
1708
1837
|
}
|
|
1709
1838
|
|
|
1710
1839
|
// Live windows back-to-front
|
|
1711
|
-
ForEach(Array(
|
|
1712
|
-
windowTile(win: win, editor: editor,
|
|
1840
|
+
ForEach(Array(canvasWindows.sorted(by: { $0.zIndex > $1.zIndex }).enumerated()), id: \.element.id) { _, win in
|
|
1841
|
+
windowTile(win: win, editor: editor, metrics: metrics)
|
|
1713
1842
|
}
|
|
1714
1843
|
}
|
|
1715
|
-
.frame(width:
|
|
1716
|
-
.offset(x:
|
|
1844
|
+
.frame(width: metrics.mapSize.width, height: metrics.mapSize.height)
|
|
1845
|
+
.offset(x: metrics.centerOffset.x + panOffset.x, y: metrics.centerOffset.y + panOffset.y)
|
|
1717
1846
|
.onAppear {
|
|
1718
|
-
syncCanvasGeometry(editor: editor,
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
bboxOrigin: bboxOriginPt)
|
|
1723
|
-
}
|
|
1724
|
-
.onChange(of: geo.size) { _ in
|
|
1725
|
-
let newFitScale = min((geo.size.width - 24) / screenW, (geo.size.height - 16) / screenH)
|
|
1726
|
-
let newEffScale = newFitScale * zoomLevel
|
|
1727
|
-
let newMapW = screenW * newEffScale
|
|
1728
|
-
let newMapH = screenH * newEffScale
|
|
1729
|
-
let newCX = (geo.size.width - newMapW) / 2
|
|
1730
|
-
let newCY = (geo.size.height - newMapH) / 2
|
|
1731
|
-
syncCanvasGeometry(editor: editor, fitScale: newFitScale, scale: newEffScale,
|
|
1732
|
-
offsetX: newCX, offsetY: newCY,
|
|
1733
|
-
viewportSize: CGSize(width: max(geo.size.width - 16, 1), height: max(geo.size.height - 16, 1)),
|
|
1734
|
-
screenSize: CGSize(width: screenW, height: screenH),
|
|
1735
|
-
bboxOrigin: bboxOriginPt)
|
|
1736
|
-
}
|
|
1737
|
-
.onChange(of: bbox) { _ in
|
|
1738
|
-
syncCanvasGeometry(editor: editor, fitScale: fitScale, scale: effScale,
|
|
1739
|
-
offsetX: centerX, offsetY: centerY,
|
|
1740
|
-
viewportSize: CGSize(width: max(geo.size.width - 16, 1), height: max(geo.size.height - 16, 1)),
|
|
1741
|
-
screenSize: CGSize(width: screenW, height: screenH),
|
|
1742
|
-
bboxOrigin: bboxOriginPt)
|
|
1743
|
-
}
|
|
1744
|
-
.onChange(of: zoomLevel) { _ in
|
|
1745
|
-
syncCanvasGeometry(editor: editor, fitScale: fitScale, scale: effScale,
|
|
1746
|
-
offsetX: centerX, offsetY: centerY,
|
|
1747
|
-
viewportSize: CGSize(width: max(geo.size.width - 16, 1), height: max(geo.size.height - 16, 1)),
|
|
1748
|
-
screenSize: CGSize(width: screenW, height: screenH),
|
|
1749
|
-
bboxOrigin: bboxOriginPt)
|
|
1750
|
-
}
|
|
1751
|
-
.onChange(of: editor?.canvasNavigationRevision ?? 0) { _ in
|
|
1752
|
-
syncCanvasGeometry(editor: editor, fitScale: fitScale, scale: effScale,
|
|
1753
|
-
offsetX: centerX, offsetY: centerY,
|
|
1754
|
-
viewportSize: CGSize(width: max(geo.size.width - 16, 1), height: max(geo.size.height - 16, 1)),
|
|
1755
|
-
screenSize: CGSize(width: screenW, height: screenH),
|
|
1756
|
-
bboxOrigin: bboxOriginPt)
|
|
1847
|
+
syncCanvasGeometry(editor: editor, metrics: metrics)
|
|
1848
|
+
}
|
|
1849
|
+
.onChange(of: syncKey) { _ in
|
|
1850
|
+
syncCanvasGeometry(editor: editor, metrics: metrics)
|
|
1757
1851
|
}
|
|
1758
1852
|
}
|
|
1759
1853
|
.padding(8)
|
|
@@ -1805,7 +1899,7 @@ struct ScreenMapView: View {
|
|
|
1805
1899
|
|
|
1806
1900
|
// MARK: - Display Backgrounds
|
|
1807
1901
|
|
|
1808
|
-
private func focusedDisplayBackground(
|
|
1902
|
+
private func focusedDisplayBackground(mapSize: CGSize) -> some View {
|
|
1809
1903
|
ZStack(alignment: .topLeading) {
|
|
1810
1904
|
RoundedRectangle(cornerRadius: 6)
|
|
1811
1905
|
.fill(Palette.bg.opacity(0.5))
|
|
@@ -1816,15 +1910,12 @@ struct ScreenMapView: View {
|
|
|
1816
1910
|
.contentShape(Rectangle())
|
|
1817
1911
|
.onTapGesture { controller.clearSelection() }
|
|
1818
1912
|
}
|
|
1819
|
-
.frame(width:
|
|
1913
|
+
.frame(width: mapSize.width, height: mapSize.height)
|
|
1820
1914
|
}
|
|
1821
1915
|
|
|
1822
|
-
private func multiDisplayBackgrounds(displays: [DisplayGeometry], editor: ScreenMapEditorState?,
|
|
1916
|
+
private func multiDisplayBackgrounds(displays: [DisplayGeometry], editor: ScreenMapEditorState?, metrics: CanvasMetrics) -> some View {
|
|
1823
1917
|
ForEach(displays, id: \.index) { disp in
|
|
1824
|
-
let
|
|
1825
|
-
let dy = (disp.cgRect.origin.y - bboxOrigin.y) * effScale
|
|
1826
|
-
let dw = disp.cgRect.width * effScale
|
|
1827
|
-
let dh = disp.cgRect.height * effScale
|
|
1918
|
+
let frame = metrics.mapRect(for: disp.cgRect, minimumSize: 12)
|
|
1828
1919
|
let bezel: CGFloat = 3
|
|
1829
1920
|
|
|
1830
1921
|
ZStack {
|
|
@@ -1862,15 +1953,14 @@ struct ScreenMapView: View {
|
|
|
1862
1953
|
}
|
|
1863
1954
|
.contentShape(Rectangle())
|
|
1864
1955
|
.onTapGesture {
|
|
1865
|
-
|
|
1866
|
-
controller.objectWillChange.send()
|
|
1956
|
+
controller.setDisplayFocus(disp.index)
|
|
1867
1957
|
}
|
|
1868
|
-
.frame(width:
|
|
1869
|
-
.offset(x:
|
|
1958
|
+
.frame(width: frame.width, height: frame.height)
|
|
1959
|
+
.offset(x: frame.minX, y: frame.minY)
|
|
1870
1960
|
}
|
|
1871
1961
|
}
|
|
1872
1962
|
|
|
1873
|
-
private func singleDisplayBackground(
|
|
1963
|
+
private func singleDisplayBackground(mapSize: CGSize) -> some View {
|
|
1874
1964
|
ZStack(alignment: .topLeading) {
|
|
1875
1965
|
RoundedRectangle(cornerRadius: 6)
|
|
1876
1966
|
.fill(Palette.bg.opacity(0.5))
|
|
@@ -1882,18 +1972,16 @@ struct ScreenMapView: View {
|
|
|
1882
1972
|
.onTapGesture { controller.clearSelection() }
|
|
1883
1973
|
|
|
1884
1974
|
}
|
|
1885
|
-
.frame(width:
|
|
1975
|
+
.frame(width: mapSize.width, height: mapSize.height)
|
|
1886
1976
|
}
|
|
1887
1977
|
|
|
1888
1978
|
// MARK: - Window Tile
|
|
1889
1979
|
|
|
1890
1980
|
@ViewBuilder
|
|
1891
|
-
private func windowTile(win: ScreenMapWindowEntry, editor: ScreenMapEditorState?,
|
|
1892
|
-
let
|
|
1893
|
-
let
|
|
1894
|
-
let
|
|
1895
|
-
let w = max(f.width * scale, 4)
|
|
1896
|
-
let h = max(f.height * scale, 4)
|
|
1981
|
+
private func windowTile(win: ScreenMapWindowEntry, editor: ScreenMapEditorState?, metrics: CanvasMetrics) -> some View {
|
|
1982
|
+
let rect = metrics.mapRect(for: win.virtualFrame)
|
|
1983
|
+
let w = rect.width
|
|
1984
|
+
let h = rect.height
|
|
1897
1985
|
let isSelected = controller.selectedWindowIds.contains(win.id)
|
|
1898
1986
|
let isDragging = editor?.draggingWindowId == win.id
|
|
1899
1987
|
let isInActiveLayer = editor?.isLayerSelected(win.layer) ?? true
|
|
@@ -1999,7 +2087,7 @@ struct ScreenMapView: View {
|
|
|
1999
2087
|
.shadow(color: Self.shelfGreen.opacity(0.5), radius: 6)
|
|
2000
2088
|
}
|
|
2001
2089
|
}
|
|
2002
|
-
.offset(x:
|
|
2090
|
+
.offset(x: rect.minX, y: rect.minY)
|
|
2003
2091
|
.opacity(isInActiveLayer ? 1.0 : 0.3)
|
|
2004
2092
|
.shadow(color: isDragging ? Palette.running.opacity(0.4) : .clear,
|
|
2005
2093
|
radius: isDragging ? 6 : 0)
|
|
@@ -2088,11 +2176,7 @@ struct ScreenMapView: View {
|
|
|
2088
2176
|
let pct = Int(editor.zoomLevel * 100)
|
|
2089
2177
|
return HStack(spacing: 0) {
|
|
2090
2178
|
Button {
|
|
2091
|
-
|
|
2092
|
-
editor.activeViewportPreset = nil
|
|
2093
|
-
editor.zoomLevel = newZoom
|
|
2094
|
-
editor.objectWillChange.send()
|
|
2095
|
-
controller.objectWillChange.send()
|
|
2179
|
+
controller.adjustZoom(by: -0.25)
|
|
2096
2180
|
} label: {
|
|
2097
2181
|
Image(systemName: "minus")
|
|
2098
2182
|
.font(.system(size: 9, weight: .medium))
|
|
@@ -2116,11 +2200,7 @@ struct ScreenMapView: View {
|
|
|
2116
2200
|
Rectangle().fill(Palette.border).frame(width: 0.5, height: 12)
|
|
2117
2201
|
|
|
2118
2202
|
Button {
|
|
2119
|
-
|
|
2120
|
-
editor.activeViewportPreset = nil
|
|
2121
|
-
editor.zoomLevel = newZoom
|
|
2122
|
-
editor.objectWillChange.send()
|
|
2123
|
-
controller.objectWillChange.send()
|
|
2203
|
+
controller.adjustZoom(by: 0.25)
|
|
2124
2204
|
} label: {
|
|
2125
2205
|
Image(systemName: "plus")
|
|
2126
2206
|
.font(.system(size: 9, weight: .medium))
|
|
@@ -2325,164 +2405,115 @@ struct ScreenMapView: View {
|
|
|
2325
2405
|
@ViewBuilder
|
|
2326
2406
|
private func sidebarMiniMap(editor: ScreenMapEditorState) -> some View {
|
|
2327
2407
|
let displays = editor.displays
|
|
2328
|
-
let windows = editor.
|
|
2329
|
-
let world = editor.canvasWorldBounds
|
|
2330
|
-
let viewport = editor.viewportWorldRect
|
|
2408
|
+
let windows = editor.renderedCanvasWindows
|
|
2331
2409
|
let miniW: CGFloat = sidebarWidth - 28
|
|
2332
2410
|
let miniH: CGFloat = 118
|
|
2333
|
-
let
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
let drawH = world.height * scale
|
|
2338
|
-
let offsetX = (miniW - drawW) / 2
|
|
2339
|
-
let offsetY = (miniH - drawH) / 2
|
|
2340
|
-
|
|
2341
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
2342
|
-
HStack(spacing: 6) {
|
|
2343
|
-
Text("MAP")
|
|
2344
|
-
.font(Typo.monoBold(8))
|
|
2345
|
-
.foregroundColor(Palette.textMuted)
|
|
2346
|
-
Spacer()
|
|
2347
|
-
Text("drag to pan")
|
|
2348
|
-
.font(Typo.mono(7))
|
|
2349
|
-
.foregroundColor(Palette.textMuted)
|
|
2350
|
-
}
|
|
2411
|
+
let metrics = MiniMapMetrics(
|
|
2412
|
+
worldBounds: editor.canvasWorldBounds,
|
|
2413
|
+
canvasSize: CGSize(width: miniW, height: miniH)
|
|
2414
|
+
)
|
|
2351
2415
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2416
|
+
sidebarPanel {
|
|
2417
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
2418
|
+
HStack(spacing: 6) {
|
|
2419
|
+
Text("MAP")
|
|
2420
|
+
.font(Typo.monoBold(8))
|
|
2421
|
+
.foregroundColor(Palette.textMuted)
|
|
2422
|
+
Spacer()
|
|
2423
|
+
Text("drag to pan")
|
|
2424
|
+
.font(Typo.mono(7))
|
|
2425
|
+
.foregroundColor(Palette.textMuted)
|
|
2426
|
+
}
|
|
2355
2427
|
|
|
2356
2428
|
ZStack(alignment: .topLeading) {
|
|
2357
|
-
RoundedRectangle(cornerRadius:
|
|
2358
|
-
.fill(
|
|
2359
|
-
.frame(width: drawW, height: drawH)
|
|
2360
|
-
.offset(x: offsetX, y: offsetY)
|
|
2429
|
+
RoundedRectangle(cornerRadius: 6)
|
|
2430
|
+
.fill(Color.black.opacity(0.28))
|
|
2361
2431
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
let isFocused = editor.focusedDisplayIndex == nil || editor.focusedDisplayIndex == disp.index
|
|
2432
|
+
ZStack(alignment: .topLeading) {
|
|
2433
|
+
RoundedRectangle(cornerRadius: 5)
|
|
2434
|
+
.fill(Palette.bg.opacity(0.35))
|
|
2435
|
+
.frame(width: metrics.drawSize.width, height: metrics.drawSize.height)
|
|
2436
|
+
.offset(x: metrics.offset.x, y: metrics.offset.y)
|
|
2368
2437
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
.
|
|
2372
|
-
RoundedRectangle(cornerRadius: 3)
|
|
2373
|
-
.strokeBorder(
|
|
2374
|
-
editor.focusedDisplayIndex == disp.index ? Palette.running.opacity(0.55) : Color.white.opacity(0.12),
|
|
2375
|
-
lineWidth: editor.focusedDisplayIndex == disp.index ? 1 : 0.5
|
|
2376
|
-
)
|
|
2377
|
-
)
|
|
2378
|
-
.frame(width: max(dw, 12), height: max(dh, 12))
|
|
2379
|
-
.offset(x: dx, y: dy)
|
|
2380
|
-
}
|
|
2438
|
+
ForEach(displays, id: \.index) { disp in
|
|
2439
|
+
let rect = metrics.rect(for: disp.cgRect, minimumSize: 12)
|
|
2440
|
+
let isFocused = editor.focusedDisplayIndex == nil || editor.focusedDisplayIndex == disp.index
|
|
2381
2441
|
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2442
|
+
RoundedRectangle(cornerRadius: 3)
|
|
2443
|
+
.fill(isFocused ? Color.white.opacity(0.05) : Color.white.opacity(0.02))
|
|
2444
|
+
.overlay(
|
|
2445
|
+
RoundedRectangle(cornerRadius: 3)
|
|
2446
|
+
.strokeBorder(
|
|
2447
|
+
editor.focusedDisplayIndex == disp.index ? Palette.running.opacity(0.55) : Color.white.opacity(0.12),
|
|
2448
|
+
lineWidth: editor.focusedDisplayIndex == disp.index ? 1 : 0.5
|
|
2449
|
+
)
|
|
2450
|
+
)
|
|
2451
|
+
.frame(width: rect.width, height: rect.height)
|
|
2452
|
+
.offset(x: rect.minX, y: rect.minY)
|
|
2453
|
+
}
|
|
2389
2454
|
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
.
|
|
2393
|
-
RoundedRectangle(cornerRadius: 1.5)
|
|
2394
|
-
.strokeBorder(isSelected ? Palette.running.opacity(0.85) : Color.white.opacity(0.12), lineWidth: isSelected ? 1 : 0.5)
|
|
2395
|
-
)
|
|
2396
|
-
.frame(width: w, height: h)
|
|
2397
|
-
.offset(x: x, y: y)
|
|
2398
|
-
}
|
|
2455
|
+
ForEach(Array(windows.sorted(by: { $0.zIndex > $1.zIndex }).enumerated()), id: \.element.id) { _, win in
|
|
2456
|
+
let rect = metrics.rect(for: win.virtualFrame, minimumSize: 2)
|
|
2457
|
+
let isSelected = controller.selectedWindowIds.contains(win.id)
|
|
2399
2458
|
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2459
|
+
RoundedRectangle(cornerRadius: 1.5)
|
|
2460
|
+
.fill((isSelected ? Palette.running : Self.layerColor(for: win.layer)).opacity(isSelected ? 0.35 : 0.18))
|
|
2461
|
+
.overlay(
|
|
2462
|
+
RoundedRectangle(cornerRadius: 1.5)
|
|
2463
|
+
.strokeBorder(isSelected ? Palette.running.opacity(0.85) : Color.white.opacity(0.12), lineWidth: isSelected ? 1 : 0.5)
|
|
2464
|
+
)
|
|
2465
|
+
.frame(width: rect.width, height: rect.height)
|
|
2466
|
+
.offset(x: rect.minX, y: rect.minY)
|
|
2467
|
+
}
|
|
2404
2468
|
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
.frame(width: miniW, height: miniH)
|
|
2416
|
-
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
2417
|
-
.contentShape(Rectangle())
|
|
2418
|
-
.gesture(
|
|
2419
|
-
DragGesture(minimumDistance: 0)
|
|
2420
|
-
.onChanged { value in
|
|
2421
|
-
let localX = min(max(value.location.x - offsetX, 0), drawW)
|
|
2422
|
-
let localY = min(max(value.location.y - offsetY, 0), drawH)
|
|
2423
|
-
let worldPoint = CGPoint(
|
|
2424
|
-
x: world.origin.x + localX / max(scale, 0.0001),
|
|
2425
|
-
y: world.origin.y + localY / max(scale, 0.0001)
|
|
2426
|
-
)
|
|
2427
|
-
controller.recenterViewport(at: worldPoint)
|
|
2469
|
+
let viewportRect = metrics.rect(for: editor.viewportWorldRect, minimumSize: 12)
|
|
2470
|
+
|
|
2471
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2472
|
+
.strokeBorder(Palette.running.opacity(0.9), lineWidth: 1.25)
|
|
2473
|
+
.background(
|
|
2474
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2475
|
+
.fill(Palette.running.opacity(0.08))
|
|
2476
|
+
)
|
|
2477
|
+
.frame(width: viewportRect.width, height: viewportRect.height)
|
|
2478
|
+
.offset(x: viewportRect.minX, y: viewportRect.minY)
|
|
2428
2479
|
}
|
|
2429
|
-
|
|
2480
|
+
}
|
|
2481
|
+
.frame(width: miniW, height: miniH)
|
|
2482
|
+
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
2483
|
+
.contentShape(Rectangle())
|
|
2484
|
+
.gesture(
|
|
2485
|
+
DragGesture(minimumDistance: 0)
|
|
2486
|
+
.onChanged { value in
|
|
2487
|
+
controller.recenterViewport(at: metrics.worldPoint(for: value.location))
|
|
2488
|
+
}
|
|
2489
|
+
)
|
|
2430
2490
|
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2491
|
+
HStack(spacing: 6) {
|
|
2492
|
+
mapScopePill("ALL", isActive: editor.focusedDisplayIndex == nil) {
|
|
2493
|
+
controller.focusCanvas(on: editor.canvasWorldBounds, focusDisplay: nil, zoomToFit: true)
|
|
2494
|
+
}
|
|
2495
|
+
ForEach(editor.spatialDisplayOrder, id: \.index) { disp in
|
|
2496
|
+
mapScopePill("\(editor.spatialNumber(for: disp.index))", isActive: editor.focusedDisplayIndex == disp.index) {
|
|
2497
|
+
controller.focusCanvas(
|
|
2498
|
+
on: editor.displayRegion(for: disp.index)?.rect ?? disp.cgRect,
|
|
2499
|
+
focusDisplay: disp.index,
|
|
2500
|
+
zoomToFit: true
|
|
2501
|
+
)
|
|
2502
|
+
}
|
|
2442
2503
|
}
|
|
2443
2504
|
}
|
|
2444
2505
|
}
|
|
2445
2506
|
}
|
|
2446
|
-
.padding(6)
|
|
2447
|
-
.background(
|
|
2448
|
-
RoundedRectangle(cornerRadius: 6)
|
|
2449
|
-
.fill(Color.black.opacity(0.4))
|
|
2450
|
-
.overlay(
|
|
2451
|
-
RoundedRectangle(cornerRadius: 6)
|
|
2452
|
-
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
2453
|
-
)
|
|
2454
|
-
)
|
|
2455
2507
|
}
|
|
2456
2508
|
|
|
2457
2509
|
private func canvasExplorer(editor: ScreenMapEditorState) -> some View {
|
|
2458
2510
|
let regions = editor.canvasExplorerRegions
|
|
2459
2511
|
|
|
2460
2512
|
return VStack(alignment: .leading, spacing: 4) {
|
|
2461
|
-
HStack {
|
|
2462
|
-
Text("EXPLORER")
|
|
2463
|
-
.font(Typo.monoBold(8))
|
|
2464
|
-
.foregroundColor(Palette.textMuted)
|
|
2465
|
-
Spacer()
|
|
2466
|
-
if let viewport = controller.editor?.viewportWorldRect {
|
|
2467
|
-
Text("\(Int(viewport.midX)),\(Int(viewport.midY))")
|
|
2468
|
-
.font(Typo.mono(7))
|
|
2469
|
-
.foregroundColor(Palette.textMuted)
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
2513
|
ForEach(regions.prefix(8)) { region in
|
|
2474
2514
|
canvasExplorerRow(region: region)
|
|
2475
2515
|
}
|
|
2476
2516
|
}
|
|
2477
|
-
.padding(6)
|
|
2478
|
-
.background(
|
|
2479
|
-
RoundedRectangle(cornerRadius: 6)
|
|
2480
|
-
.fill(Color.black.opacity(0.4))
|
|
2481
|
-
.overlay(
|
|
2482
|
-
RoundedRectangle(cornerRadius: 6)
|
|
2483
|
-
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
2484
|
-
)
|
|
2485
|
-
)
|
|
2486
2517
|
}
|
|
2487
2518
|
|
|
2488
2519
|
private func canvasExplorerRow(region: ScreenMapCanvasRegion) -> some View {
|
|
@@ -2591,16 +2622,27 @@ struct ScreenMapView: View {
|
|
|
2591
2622
|
|
|
2592
2623
|
// MARK: - Helpers
|
|
2593
2624
|
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2625
|
+
@ViewBuilder
|
|
2626
|
+
private func sidebarPanel<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
|
2627
|
+
content()
|
|
2628
|
+
.padding(6)
|
|
2629
|
+
.background(
|
|
2630
|
+
RoundedRectangle(cornerRadius: 6)
|
|
2631
|
+
.fill(Color.black.opacity(0.4))
|
|
2632
|
+
.overlay(
|
|
2633
|
+
RoundedRectangle(cornerRadius: 6)
|
|
2634
|
+
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
2635
|
+
)
|
|
2636
|
+
)
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
private func syncCanvasGeometry(editor: ScreenMapEditorState?, metrics: CanvasMetrics) {
|
|
2640
|
+
editor?.fitScale = metrics.fitScale
|
|
2641
|
+
editor?.scale = metrics.effectiveScale
|
|
2642
|
+
editor?.mapOrigin = metrics.centerOffset
|
|
2643
|
+
editor?.viewportSize = metrics.syncedViewportSize
|
|
2644
|
+
editor?.screenSize = metrics.worldBounds.size
|
|
2645
|
+
editor?.bboxOrigin = metrics.worldBounds.origin
|
|
2604
2646
|
controller.applyPendingCanvasNavigationIfNeeded()
|
|
2605
2647
|
}
|
|
2606
2648
|
|
|
@@ -2755,15 +2797,13 @@ struct ScreenMapView: View {
|
|
|
2755
2797
|
return nil
|
|
2756
2798
|
}
|
|
2757
2799
|
|
|
2758
|
-
|
|
2759
|
-
|
|
2800
|
+
let flippedPt = flippedScreenPoint(event)
|
|
2801
|
+
if let editor = controller.editor,
|
|
2802
|
+
let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor),
|
|
2803
|
+
hoveredWindowId == hit.id {
|
|
2804
|
+
screenMapClickWindowId = hit.id
|
|
2760
2805
|
screenMapClickPoint = event.locationInWindow
|
|
2761
|
-
|
|
2762
|
-
if let hit = screenMapHitTestWithRect(flippedScreenPt: flippedPt, editor: editor) {
|
|
2763
|
-
editor.canvasDragMode = detectDragMode(mapPoint: hit.mapPoint, windowMapRect: hit.mapRect)
|
|
2764
|
-
} else {
|
|
2765
|
-
editor.canvasDragMode = .move
|
|
2766
|
-
}
|
|
2806
|
+
editor.canvasDragMode = detectDragMode(mapPoint: hit.mapPoint, windowMapRect: hit.mapRect)
|
|
2767
2807
|
} else {
|
|
2768
2808
|
screenMapClickWindowId = nil
|
|
2769
2809
|
}
|
|
@@ -2777,8 +2817,6 @@ struct ScreenMapView: View {
|
|
|
2777
2817
|
let dy = event.locationInWindow.y - start.y
|
|
2778
2818
|
editor.activeViewportPreset = nil
|
|
2779
2819
|
editor.panOffset = CGPoint(x: spaceDragPanStart.x + dx, y: spaceDragPanStart.y - dy)
|
|
2780
|
-
editor.objectWillChange.send()
|
|
2781
|
-
controller.objectWillChange.send()
|
|
2782
2820
|
return nil
|
|
2783
2821
|
}
|
|
2784
2822
|
|
|
@@ -2849,8 +2887,6 @@ struct ScreenMapView: View {
|
|
|
2849
2887
|
}
|
|
2850
2888
|
|
|
2851
2889
|
editor.syncLayoutFrame(at: idx, to: newFrame)
|
|
2852
|
-
editor.objectWillChange.send()
|
|
2853
|
-
controller.objectWillChange.send()
|
|
2854
2890
|
return nil
|
|
2855
2891
|
}
|
|
2856
2892
|
|
|
@@ -2866,7 +2902,6 @@ struct ScreenMapView: View {
|
|
|
2866
2902
|
editor.draggingWindowId = nil
|
|
2867
2903
|
editor.dragStartFrame = nil
|
|
2868
2904
|
editor.canvasDragMode = .move
|
|
2869
|
-
editor.objectWillChange.send()
|
|
2870
2905
|
}
|
|
2871
2906
|
screenMapClickWindowId = nil
|
|
2872
2907
|
}
|
|
@@ -2882,11 +2917,11 @@ struct ScreenMapView: View {
|
|
|
2882
2917
|
let canvasRect = CGRect(origin: screenMapCanvasOrigin, size: screenMapCanvasSize)
|
|
2883
2918
|
guard canvasRect.contains(flippedPt) else { return event }
|
|
2884
2919
|
|
|
2885
|
-
if let
|
|
2886
|
-
if !controller.isSelected(
|
|
2887
|
-
controller.selectSingle(
|
|
2920
|
+
if let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor) {
|
|
2921
|
+
if !controller.isSelected(hit.id) {
|
|
2922
|
+
controller.selectSingle(hit.id)
|
|
2888
2923
|
}
|
|
2889
|
-
showLayerContextMenu(for:
|
|
2924
|
+
showLayerContextMenu(for: hit.id, at: event.locationInWindow, in: eventWindow, editor: editor)
|
|
2890
2925
|
return nil
|
|
2891
2926
|
}
|
|
2892
2927
|
return event
|
|
@@ -2938,16 +2973,12 @@ struct ScreenMapView: View {
|
|
|
2938
2973
|
editor.activeViewportPreset = nil
|
|
2939
2974
|
editor.zoomLevel = newZoom
|
|
2940
2975
|
editor.panOffset = CGPoint(x: newPanX, y: newPanY)
|
|
2941
|
-
editor.objectWillChange.send()
|
|
2942
|
-
controller.objectWillChange.send()
|
|
2943
2976
|
} else {
|
|
2944
2977
|
editor.activeViewportPreset = nil
|
|
2945
2978
|
editor.panOffset = CGPoint(
|
|
2946
2979
|
x: editor.panOffset.x + event.scrollingDeltaX,
|
|
2947
2980
|
y: editor.panOffset.y - event.scrollingDeltaY
|
|
2948
2981
|
)
|
|
2949
|
-
editor.objectWillChange.send()
|
|
2950
|
-
controller.objectWillChange.send()
|
|
2951
2982
|
}
|
|
2952
2983
|
return nil
|
|
2953
2984
|
}
|
|
@@ -2967,7 +2998,7 @@ struct ScreenMapView: View {
|
|
|
2967
2998
|
return event
|
|
2968
2999
|
}
|
|
2969
3000
|
|
|
2970
|
-
if let hit =
|
|
3001
|
+
if let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor) {
|
|
2971
3002
|
let mode = detectDragMode(mapPoint: hit.mapPoint, windowMapRect: hit.mapRect)
|
|
2972
3003
|
if mode != editor.currentCursorMode {
|
|
2973
3004
|
if editor.currentCursorMode != .move { NSCursor.pop() }
|
|
@@ -3010,64 +3041,20 @@ struct ScreenMapView: View {
|
|
|
3010
3041
|
|
|
3011
3042
|
// MARK: - Hit Test / Coordinate Conversion
|
|
3012
3043
|
|
|
3013
|
-
private func
|
|
3014
|
-
let
|
|
3015
|
-
|
|
3016
|
-
let panOffset = editor.panOffset
|
|
3017
|
-
guard effScale > 0 else { return nil }
|
|
3018
|
-
|
|
3044
|
+
private func canvasHit(flippedScreenPt: CGPoint, editor: ScreenMapEditorState) -> CanvasHit? {
|
|
3045
|
+
let projection = CanvasProjection(editor: editor)
|
|
3046
|
+
guard projection.scale > 0 else { return nil }
|
|
3019
3047
|
let canvasLocal = CGPoint(
|
|
3020
3048
|
x: flippedScreenPt.x - screenMapCanvasOrigin.x,
|
|
3021
3049
|
y: flippedScreenPt.y - screenMapCanvasOrigin.y
|
|
3022
3050
|
)
|
|
3023
|
-
let mapPoint =
|
|
3024
|
-
|
|
3025
|
-
y: canvasLocal.y - 8 - origin.y - panOffset.y
|
|
3026
|
-
)
|
|
3027
|
-
|
|
3028
|
-
let bboxOrig = editor.bboxOrigin
|
|
3029
|
-
let windowPool = editor.focusedDisplayIndex != nil ? editor.focusedVisibleWindows : editor.windows
|
|
3030
|
-
let sorted = windowPool.sorted(by: { $0.zIndex < $1.zIndex })
|
|
3051
|
+
let mapPoint = projection.mapPoint(forCanvasPoint: canvasLocal)
|
|
3052
|
+
let sorted = editor.renderedCanvasWindows.sorted(by: { $0.zIndex < $1.zIndex })
|
|
3031
3053
|
for win in sorted {
|
|
3032
|
-
let
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
width: max(f.width * effScale, 4),
|
|
3037
|
-
height: max(f.height * effScale, 4)
|
|
3038
|
-
)
|
|
3039
|
-
if mapRect.contains(mapPoint) { return win.id }
|
|
3040
|
-
}
|
|
3041
|
-
return nil
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
private func screenMapHitTestWithRect(flippedScreenPt: CGPoint, editor: ScreenMapEditorState) -> (id: UInt32, mapRect: CGRect, mapPoint: CGPoint)? {
|
|
3045
|
-
let effScale = editor.effectiveScale
|
|
3046
|
-
let origin = editor.mapOrigin
|
|
3047
|
-
let panOff = editor.panOffset
|
|
3048
|
-
guard effScale > 0 else { return nil }
|
|
3049
|
-
|
|
3050
|
-
let canvasLocal = CGPoint(
|
|
3051
|
-
x: flippedScreenPt.x - screenMapCanvasOrigin.x,
|
|
3052
|
-
y: flippedScreenPt.y - screenMapCanvasOrigin.y
|
|
3053
|
-
)
|
|
3054
|
-
let mapPoint = CGPoint(
|
|
3055
|
-
x: canvasLocal.x - 8 - origin.x - panOff.x,
|
|
3056
|
-
y: canvasLocal.y - 8 - origin.y - panOff.y
|
|
3057
|
-
)
|
|
3058
|
-
|
|
3059
|
-
let bboxOrig = editor.bboxOrigin
|
|
3060
|
-
let windowPool = editor.focusedDisplayIndex != nil ? editor.focusedVisibleWindows : editor.windows
|
|
3061
|
-
let sorted = windowPool.sorted(by: { $0.zIndex < $1.zIndex })
|
|
3062
|
-
for win in sorted {
|
|
3063
|
-
let f = win.virtualFrame
|
|
3064
|
-
let mapRect = CGRect(
|
|
3065
|
-
x: (f.origin.x - bboxOrig.x) * effScale,
|
|
3066
|
-
y: (f.origin.y - bboxOrig.y) * effScale,
|
|
3067
|
-
width: max(f.width * effScale, 4),
|
|
3068
|
-
height: max(f.height * effScale, 4)
|
|
3069
|
-
)
|
|
3070
|
-
if mapRect.contains(mapPoint) { return (win.id, mapRect, mapPoint) }
|
|
3054
|
+
let mapRect = projection.mapRect(for: win.virtualFrame)
|
|
3055
|
+
if mapRect.contains(mapPoint) {
|
|
3056
|
+
return CanvasHit(id: win.id, mapRect: mapRect, mapPoint: mapPoint)
|
|
3057
|
+
}
|
|
3071
3058
|
}
|
|
3072
3059
|
return nil
|
|
3073
3060
|
}
|
|
@@ -3181,7 +3168,6 @@ final class ScreenMapMenuTarget: NSObject {
|
|
|
3181
3168
|
@objc func performLayerMove(_ sender: NSMenuItem) {
|
|
3182
3169
|
guard let action = sender.representedObject as? ScreenMapLayerMenuAction else { return }
|
|
3183
3170
|
action.editor.reassignLayer(windowId: action.windowId, toLayer: action.targetLayer, fitToAvailable: true)
|
|
3184
|
-
action.controller.objectWillChange.send()
|
|
3185
3171
|
}
|
|
3186
3172
|
|
|
3187
3173
|
@objc func performFocus(_ sender: NSMenuItem) {
|