@lattices/cli 0.3.0 → 0.4.1
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 +85 -9
- package/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Package.swift +8 -1
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +45 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +189 -6
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +802 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +49 -9
- package/app/Sources/IntentEngine.swift +962 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1275 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +58 -45
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +740 -0
- package/bin/lattices-app.ts +338 -0
- package/bin/lattices-dev +208 -0
- package/bin/{lattices.js → lattices.ts} +777 -140
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +29 -11
- package/bin/client.js +0 -4
- package/bin/lattices-app.js +0 -221
|
@@ -7,6 +7,9 @@ struct ScreenMapView: View {
|
|
|
7
7
|
@ObservedObject var controller: ScreenMapController
|
|
8
8
|
var onNavigate: ((AppPage) -> Void)? = nil
|
|
9
9
|
@ObservedObject private var daemon = DaemonServer.shared
|
|
10
|
+
@ObservedObject private var handsOff = HandsOffSession.shared
|
|
11
|
+
@ObservedObject private var diagnosticLog = DiagnosticLog.shared
|
|
12
|
+
@StateObject private var piChat = PiChatSession.shared
|
|
10
13
|
@State private var eventMonitor: Any?
|
|
11
14
|
@State private var mouseDownMonitor: Any?
|
|
12
15
|
@State private var mouseDragMonitor: Any?
|
|
@@ -28,7 +31,7 @@ struct ScreenMapView: View {
|
|
|
28
31
|
@State private var mouseMovedMonitor: Any?
|
|
29
32
|
@State private var sidebarWidth: CGFloat = 180
|
|
30
33
|
@State private var isDraggingSidebar: Bool = false
|
|
31
|
-
@State private var inspectorWidth: CGFloat =
|
|
34
|
+
@State private var inspectorWidth: CGFloat = 280
|
|
32
35
|
@State private var isDraggingInspector: Bool = false
|
|
33
36
|
@FocusState private var isSearchFieldFocused: Bool
|
|
34
37
|
@State private var searchHoveredDisplayIndex: Int? = nil
|
|
@@ -70,13 +73,13 @@ struct ScreenMapView: View {
|
|
|
70
73
|
if controller.isSearchActive, let editor = controller.editor {
|
|
71
74
|
floatingSearchOverlay(editor: editor)
|
|
72
75
|
}
|
|
73
|
-
//
|
|
76
|
+
// Viewport controls — bottom-right corner of canvas
|
|
74
77
|
if let editor = controller.editor {
|
|
75
78
|
VStack {
|
|
76
79
|
Spacer()
|
|
77
80
|
HStack {
|
|
78
81
|
Spacer()
|
|
79
|
-
|
|
82
|
+
canvasViewportDock(editor: editor)
|
|
80
83
|
.padding(10)
|
|
81
84
|
}
|
|
82
85
|
}
|
|
@@ -84,10 +87,14 @@ struct ScreenMapView: View {
|
|
|
84
87
|
}
|
|
85
88
|
if let editor = controller.editor {
|
|
86
89
|
panelResizeHandle(isActive: $isDraggingInspector, width: $inspectorWidth,
|
|
87
|
-
range:
|
|
90
|
+
range: 220...480, edge: .leading)
|
|
88
91
|
inspectorPane(editor: editor)
|
|
89
92
|
}
|
|
90
93
|
}
|
|
94
|
+
if piChat.isVisible {
|
|
95
|
+
PiChatDock(session: piChat)
|
|
96
|
+
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
97
|
+
}
|
|
91
98
|
footerBar
|
|
92
99
|
}
|
|
93
100
|
.background(Palette.bg)
|
|
@@ -111,6 +118,7 @@ struct ScreenMapView: View {
|
|
|
111
118
|
HStack(spacing: 4) {
|
|
112
119
|
Button {
|
|
113
120
|
editor.cyclePreviousDisplay()
|
|
121
|
+
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
114
122
|
controller.flash(editor.focusedDisplay?.label ?? "All displays")
|
|
115
123
|
controller.objectWillChange.send()
|
|
116
124
|
} label: {
|
|
@@ -124,6 +132,7 @@ struct ScreenMapView: View {
|
|
|
124
132
|
|
|
125
133
|
Button {
|
|
126
134
|
editor.focusDisplay(nil)
|
|
135
|
+
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
127
136
|
controller.objectWillChange.send()
|
|
128
137
|
} label: {
|
|
129
138
|
displayToolbarPill(name: "All", isActive: editor.focusedDisplayIndex == nil)
|
|
@@ -134,6 +143,7 @@ struct ScreenMapView: View {
|
|
|
134
143
|
let isActive = editor.focusedDisplayIndex == disp.index
|
|
135
144
|
Button {
|
|
136
145
|
editor.focusDisplay(disp.index)
|
|
146
|
+
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
137
147
|
controller.objectWillChange.send()
|
|
138
148
|
} label: {
|
|
139
149
|
displayToolbarPill(
|
|
@@ -147,6 +157,7 @@ struct ScreenMapView: View {
|
|
|
147
157
|
|
|
148
158
|
Button {
|
|
149
159
|
editor.cycleNextDisplay()
|
|
160
|
+
controller.focusViewportPreset(editor.activeViewportPreset ?? .main, flashView: false)
|
|
150
161
|
controller.flash(editor.focusedDisplay?.label ?? "All displays")
|
|
151
162
|
controller.objectWillChange.send()
|
|
152
163
|
} label: {
|
|
@@ -256,6 +267,8 @@ struct ScreenMapView: View {
|
|
|
256
267
|
.font(Typo.monoBold(9))
|
|
257
268
|
.foregroundColor(Palette.textMuted)
|
|
258
269
|
|
|
270
|
+
inspectorCanvasContextCard(editor: editor, selectedCount: selectedWindows.count)
|
|
271
|
+
|
|
259
272
|
if selectedWindows.isEmpty {
|
|
260
273
|
VStack(spacing: 8) {
|
|
261
274
|
Text("No Selection")
|
|
@@ -268,7 +281,7 @@ struct ScreenMapView: View {
|
|
|
268
281
|
.lineLimit(3)
|
|
269
282
|
}
|
|
270
283
|
.frame(maxWidth: .infinity)
|
|
271
|
-
.padding(.top,
|
|
284
|
+
.padding(.top, 20)
|
|
272
285
|
}
|
|
273
286
|
|
|
274
287
|
ForEach(selectedWindows) { win in
|
|
@@ -284,27 +297,87 @@ struct ScreenMapView: View {
|
|
|
284
297
|
.frame(width: inspectorWidth)
|
|
285
298
|
}
|
|
286
299
|
|
|
300
|
+
private func inspectorCanvasContextCard(editor: ScreenMapEditorState, selectedCount: Int) -> some View {
|
|
301
|
+
let viewport = editor.viewportWorldRect
|
|
302
|
+
let world = editor.canvasWorldBounds
|
|
303
|
+
let scope = editor.focusedDisplay.map { "\(editor.spatialNumber(for: $0.index)). \($0.label)" } ?? "All Displays"
|
|
304
|
+
let layers = editor.selectedLayers.isEmpty
|
|
305
|
+
? "All Layers"
|
|
306
|
+
: editor.selectedLayers.sorted().map { editor.layerDisplayName(for: $0) }.joined(separator: ", ")
|
|
307
|
+
|
|
308
|
+
return VStack(alignment: .leading, spacing: 4) {
|
|
309
|
+
inspectorRow(label: "Scope", value: scope)
|
|
310
|
+
inspectorRow(label: "Layers", value: layers)
|
|
311
|
+
inspectorRow(label: "View", value: "\(Int(viewport.midX)), \(Int(viewport.midY)) · \(Int(viewport.width))×\(Int(viewport.height))")
|
|
312
|
+
inspectorRow(label: "World", value: "\(Int(world.width))×\(Int(world.height))")
|
|
313
|
+
inspectorRow(label: "Set", value: controller.activeWindowSet?.name ?? "None")
|
|
314
|
+
inspectorRow(label: "Select", value: "\(selectedCount) window\(selectedCount == 1 ? "" : "s")")
|
|
315
|
+
}
|
|
316
|
+
.padding(8)
|
|
317
|
+
.background(
|
|
318
|
+
RoundedRectangle(cornerRadius: 6)
|
|
319
|
+
.fill(Color.black.opacity(0.25))
|
|
320
|
+
.overlay(
|
|
321
|
+
RoundedRectangle(cornerRadius: 6)
|
|
322
|
+
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
287
327
|
// MARK: - Inspector Window Card
|
|
288
328
|
|
|
289
329
|
private func inspectorWindowCard(win: ScreenMapWindowEntry, editor: ScreenMapEditorState) -> some View {
|
|
290
|
-
|
|
330
|
+
let desktopEntry = DesktopModel.shared.windows[UInt32(win.id)]
|
|
331
|
+
let ocrText = OcrModel.shared.results[UInt32(win.id)]?.fullText
|
|
332
|
+
let layerTag = DesktopModel.shared.windowLayerTags[UInt32(win.id)]
|
|
333
|
+
|
|
334
|
+
return VStack(alignment: .leading, spacing: 8) {
|
|
335
|
+
// Header: app + visibility
|
|
291
336
|
HStack(spacing: 5) {
|
|
292
337
|
Circle()
|
|
293
338
|
.fill(Self.layerColor(for: win.layer))
|
|
294
339
|
.frame(width: 6, height: 6)
|
|
295
340
|
Text(win.app)
|
|
296
|
-
.font(Typo.monoBold(
|
|
341
|
+
.font(Typo.monoBold(11))
|
|
297
342
|
.foregroundColor(Palette.text)
|
|
298
343
|
.lineLimit(1)
|
|
344
|
+
Spacer()
|
|
345
|
+
if desktopEntry?.isOnScreen == true {
|
|
346
|
+
Text("visible")
|
|
347
|
+
.font(Typo.monoBold(7))
|
|
348
|
+
.foregroundColor(Palette.running)
|
|
349
|
+
.padding(.horizontal, 4)
|
|
350
|
+
.padding(.vertical, 1)
|
|
351
|
+
.background(
|
|
352
|
+
RoundedRectangle(cornerRadius: 2)
|
|
353
|
+
.fill(Palette.running.opacity(0.1))
|
|
354
|
+
)
|
|
355
|
+
}
|
|
299
356
|
}
|
|
357
|
+
|
|
358
|
+
// Title
|
|
300
359
|
if !win.title.isEmpty {
|
|
301
360
|
Text(win.title)
|
|
302
|
-
.font(Typo.mono(
|
|
361
|
+
.font(Typo.mono(10))
|
|
303
362
|
.foregroundColor(Palette.textDim)
|
|
304
363
|
.lineLimit(3)
|
|
364
|
+
.textSelection(.enabled)
|
|
305
365
|
}
|
|
366
|
+
|
|
367
|
+
// Identity
|
|
368
|
+
HStack(spacing: 10) {
|
|
369
|
+
inspectorLabel(label: "wid", value: "\(win.id)")
|
|
370
|
+
if let entry = desktopEntry {
|
|
371
|
+
inspectorLabel(label: "pid", value: "\(entry.pid)")
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Layout info
|
|
306
376
|
VStack(alignment: .leading, spacing: 3) {
|
|
307
377
|
inspectorRow(label: "Layer", value: editor.layerDisplayName(for: win.layer))
|
|
378
|
+
if let tag = layerTag {
|
|
379
|
+
inspectorRow(label: "Tag", value: tag)
|
|
380
|
+
}
|
|
308
381
|
inspectorRow(label: "Display", value: {
|
|
309
382
|
if let disp = editor.displays.first(where: { $0.index == win.displayIndex }) {
|
|
310
383
|
return "\(editor.spatialNumber(for: disp.index)). \(disp.label)"
|
|
@@ -312,15 +385,32 @@ struct ScreenMapView: View {
|
|
|
312
385
|
return "Display \(win.displayIndex)"
|
|
313
386
|
}())
|
|
314
387
|
inspectorRow(label: "Size",
|
|
315
|
-
value: "\(Int(win.
|
|
388
|
+
value: "\(Int(win.virtualFrame.width))×\(Int(win.virtualFrame.height))")
|
|
316
389
|
inspectorRow(label: "Position",
|
|
317
|
-
value: "(\(Int(win.
|
|
390
|
+
value: "(\(Int(win.virtualFrame.origin.x)), \(Int(win.virtualFrame.origin.y)))")
|
|
318
391
|
inspectorRow(label: "Z-Index", value: "\(win.zIndex)")
|
|
319
392
|
if win.hasEdits {
|
|
320
393
|
inspectorRow(label: "Original",
|
|
321
394
|
value: "\(Int(win.originalFrame.width))×\(Int(win.originalFrame.height))")
|
|
322
395
|
}
|
|
396
|
+
if let entry = desktopEntry, !entry.spaceIds.isEmpty {
|
|
397
|
+
inspectorRow(label: "Spaces", value: entry.spaceIds.map(String.init).joined(separator: ", "))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Session
|
|
402
|
+
if let session = desktopEntry?.latticesSession {
|
|
403
|
+
HStack(spacing: 4) {
|
|
404
|
+
Text("session")
|
|
405
|
+
.font(Typo.monoBold(8))
|
|
406
|
+
.foregroundColor(Palette.textMuted)
|
|
407
|
+
Text(session)
|
|
408
|
+
.font(Typo.mono(9))
|
|
409
|
+
.foregroundColor(Palette.running)
|
|
410
|
+
.lineLimit(1)
|
|
411
|
+
}
|
|
323
412
|
}
|
|
413
|
+
|
|
324
414
|
if win.hasEdits {
|
|
325
415
|
HStack(spacing: 4) {
|
|
326
416
|
Circle()
|
|
@@ -331,8 +421,32 @@ struct ScreenMapView: View {
|
|
|
331
421
|
.foregroundColor(Color.orange)
|
|
332
422
|
}
|
|
333
423
|
}
|
|
424
|
+
|
|
425
|
+
// OCR snippet
|
|
426
|
+
if let ocr = ocrText, !ocr.isEmpty {
|
|
427
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
428
|
+
Text("SCREEN TEXT")
|
|
429
|
+
.font(Typo.monoBold(8))
|
|
430
|
+
.foregroundColor(Palette.textMuted)
|
|
431
|
+
Text(String(ocr.prefix(400)))
|
|
432
|
+
.font(Typo.mono(8))
|
|
433
|
+
.foregroundColor(Palette.textMuted)
|
|
434
|
+
.lineLimit(8)
|
|
435
|
+
.textSelection(.enabled)
|
|
436
|
+
}
|
|
437
|
+
.padding(6)
|
|
438
|
+
.background(
|
|
439
|
+
RoundedRectangle(cornerRadius: 4)
|
|
440
|
+
.fill(Palette.bg.opacity(0.5))
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Window actions — contextual to this card
|
|
445
|
+
if let entry = desktopEntry {
|
|
446
|
+
windowCardActions(wid: UInt32(win.id), entry: entry)
|
|
447
|
+
}
|
|
334
448
|
}
|
|
335
|
-
.padding(
|
|
449
|
+
.padding(10)
|
|
336
450
|
.background(
|
|
337
451
|
RoundedRectangle(cornerRadius: 6)
|
|
338
452
|
.fill(Palette.surface)
|
|
@@ -343,6 +457,101 @@ struct ScreenMapView: View {
|
|
|
343
457
|
)
|
|
344
458
|
}
|
|
345
459
|
|
|
460
|
+
private func windowCardActions(wid: UInt32, entry: WindowEntry) -> some View {
|
|
461
|
+
let actions: [(key: String, label: String, action: () -> Void)] = [
|
|
462
|
+
("f", "focus", { [controller] in
|
|
463
|
+
controller.focusWindowOnScreen(wid)
|
|
464
|
+
}),
|
|
465
|
+
("h", "highlight", {
|
|
466
|
+
WindowTiler.highlightWindowById(wid: wid)
|
|
467
|
+
}),
|
|
468
|
+
("←", "tile left", {
|
|
469
|
+
WindowTiler.focusWindow(wid: wid, pid: entry.pid)
|
|
470
|
+
WindowTiler.tileWindowById(wid: wid, pid: entry.pid, to: .left)
|
|
471
|
+
}),
|
|
472
|
+
("→", "tile right", {
|
|
473
|
+
WindowTiler.focusWindow(wid: wid, pid: entry.pid)
|
|
474
|
+
WindowTiler.tileWindowById(wid: wid, pid: entry.pid, to: .right)
|
|
475
|
+
}),
|
|
476
|
+
("m", "maximize", {
|
|
477
|
+
WindowTiler.focusWindow(wid: wid, pid: entry.pid)
|
|
478
|
+
WindowTiler.tileWindowById(wid: wid, pid: entry.pid, to: .maximize)
|
|
479
|
+
}),
|
|
480
|
+
("r", "rescan", {
|
|
481
|
+
OcrModel.shared.scanSingle(wid: wid)
|
|
482
|
+
}),
|
|
483
|
+
("c", "copy info", { [controller] in
|
|
484
|
+
let info = [
|
|
485
|
+
"wid: \(wid)",
|
|
486
|
+
"app: \(entry.app)",
|
|
487
|
+
"title: \(entry.title)",
|
|
488
|
+
"pid: \(entry.pid)",
|
|
489
|
+
"frame: \(Int(entry.frame.x)),\(Int(entry.frame.y)) \(Int(entry.frame.w))×\(Int(entry.frame.h))",
|
|
490
|
+
entry.latticesSession.map { "session: \($0)" },
|
|
491
|
+
DesktopModel.shared.windowLayerTags[wid].map { "layer: \($0)" },
|
|
492
|
+
].compactMap { $0 }.joined(separator: "\n")
|
|
493
|
+
NSPasteboard.general.clearContents()
|
|
494
|
+
NSPasteboard.general.setString(info, forType: .string)
|
|
495
|
+
controller.flash("Copied")
|
|
496
|
+
}),
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
let columns = [GridItem(.flexible()), GridItem(.flexible())]
|
|
500
|
+
|
|
501
|
+
return VStack(spacing: 0) {
|
|
502
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
503
|
+
.padding(.horizontal, -10)
|
|
504
|
+
.padding(.top, 4)
|
|
505
|
+
|
|
506
|
+
LazyVGrid(columns: columns, spacing: 3) {
|
|
507
|
+
ForEach(Array(actions.enumerated()), id: \.offset) { _, item in
|
|
508
|
+
let isHov = hoveredShelfAction == "w_\(wid)_\(item.label)"
|
|
509
|
+
Button(action: item.action) {
|
|
510
|
+
HStack(spacing: 4) {
|
|
511
|
+
Text(item.key)
|
|
512
|
+
.font(.system(size: 8))
|
|
513
|
+
.foregroundColor(Self.shelfGreen)
|
|
514
|
+
.frame(width: 14)
|
|
515
|
+
Text(item.label)
|
|
516
|
+
.font(Typo.mono(8))
|
|
517
|
+
.foregroundColor(isHov ? Palette.text : Palette.textDim)
|
|
518
|
+
.lineLimit(1)
|
|
519
|
+
Spacer()
|
|
520
|
+
}
|
|
521
|
+
.padding(.horizontal, 6)
|
|
522
|
+
.padding(.vertical, 4)
|
|
523
|
+
.background(
|
|
524
|
+
RoundedRectangle(cornerRadius: 4)
|
|
525
|
+
.fill(isHov ? Palette.surfaceHov : Palette.surface)
|
|
526
|
+
.overlay(
|
|
527
|
+
RoundedRectangle(cornerRadius: 4)
|
|
528
|
+
.strokeBorder(isHov ? Palette.borderLit : Palette.border, lineWidth: 0.5)
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
.contentShape(Rectangle())
|
|
532
|
+
}
|
|
533
|
+
.buttonStyle(.plain)
|
|
534
|
+
.onHover { h in
|
|
535
|
+
let key = "w_\(wid)_\(item.label)"
|
|
536
|
+
hoveredShelfAction = h ? key : (hoveredShelfAction == key ? nil : hoveredShelfAction)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
.padding(.top, 6)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private func inspectorLabel(label: String, value: String) -> some View {
|
|
545
|
+
HStack(spacing: 3) {
|
|
546
|
+
Text(label)
|
|
547
|
+
.font(Typo.monoBold(8))
|
|
548
|
+
.foregroundColor(Palette.textMuted)
|
|
549
|
+
Text(value)
|
|
550
|
+
.font(Typo.mono(9))
|
|
551
|
+
.foregroundColor(Palette.textDim)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
346
555
|
// MARK: - Floating Search Overlay
|
|
347
556
|
|
|
348
557
|
private func floatingSearchOverlay(editor: ScreenMapEditorState) -> some View {
|
|
@@ -663,7 +872,7 @@ struct ScreenMapView: View {
|
|
|
663
872
|
}
|
|
664
873
|
}
|
|
665
874
|
|
|
666
|
-
// MARK: - Inspector
|
|
875
|
+
// MARK: - Inspector Bottom Rail
|
|
667
876
|
|
|
668
877
|
private func inspectorActionTray(editor: ScreenMapEditorState) -> some View {
|
|
669
878
|
let actions: [(key: String, label: String, action: () -> Void)] = [
|
|
@@ -672,6 +881,8 @@ struct ScreenMapView: View {
|
|
|
672
881
|
("t", "tile", { [controller] in controller.tileLayer() }),
|
|
673
882
|
("d", "distrib", { [controller] in controller.distributeVisible() }),
|
|
674
883
|
("g", "grow", { [controller] in controller.fitAvailableSpace() }),
|
|
884
|
+
("u", "set", { [controller] in controller.createWindowSetFromSelection() }),
|
|
885
|
+
("m", "project", { [controller] in controller.materializeViewport() }),
|
|
675
886
|
("c", "merge", { [controller] in controller.consolidateLayers() }),
|
|
676
887
|
("f", "flatten", { [controller] in controller.flattenLayers() }),
|
|
677
888
|
("v", "preview", { [controller] in controller.previewLayer() }),
|
|
@@ -751,8 +962,7 @@ struct ScreenMapView: View {
|
|
|
751
962
|
}
|
|
752
963
|
if isZoomed {
|
|
753
964
|
Button {
|
|
754
|
-
|
|
755
|
-
controller.flash("Fit all")
|
|
965
|
+
controller.focusViewportPreset(.overview)
|
|
756
966
|
} label: {
|
|
757
967
|
HStack(spacing: 4) {
|
|
758
968
|
Text("r")
|
|
@@ -850,10 +1060,171 @@ struct ScreenMapView: View {
|
|
|
850
1060
|
}
|
|
851
1061
|
.padding(.horizontal, 6)
|
|
852
1062
|
.padding(.bottom, 4)
|
|
1063
|
+
|
|
1064
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
1065
|
+
inspectorVoiceTray
|
|
1066
|
+
|
|
1067
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
1068
|
+
inspectorLogTray
|
|
853
1069
|
}
|
|
854
1070
|
.background(Color(red: 0.06, green: 0.06, blue: 0.07))
|
|
855
1071
|
}
|
|
856
1072
|
|
|
1073
|
+
private var inspectorVoiceStateLabel: String {
|
|
1074
|
+
switch handsOff.state {
|
|
1075
|
+
case .idle: return handsOff.lastTranscript == nil ? "ready" : "idle"
|
|
1076
|
+
case .connecting: return "connecting"
|
|
1077
|
+
case .listening: return "listening"
|
|
1078
|
+
case .thinking: return "thinking"
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private var inspectorVoiceColor: Color {
|
|
1083
|
+
switch handsOff.state {
|
|
1084
|
+
case .idle: return Palette.textMuted.opacity(0.55)
|
|
1085
|
+
case .connecting: return Palette.detach
|
|
1086
|
+
case .listening: return Palette.running
|
|
1087
|
+
case .thinking: return Palette.detach
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private var visibleDiagnosticEntries: [DiagnosticLog.Entry] {
|
|
1092
|
+
let entries = diagnosticLog.entries
|
|
1093
|
+
let tail = 8
|
|
1094
|
+
if entries.count <= tail { return entries }
|
|
1095
|
+
return Array(entries.suffix(tail))
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private var inspectorVoiceTray: some View {
|
|
1099
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
1100
|
+
HStack(spacing: 6) {
|
|
1101
|
+
Text("VOICE")
|
|
1102
|
+
.font(Typo.monoBold(8))
|
|
1103
|
+
.foregroundColor(Palette.textMuted)
|
|
1104
|
+
Spacer()
|
|
1105
|
+
Circle()
|
|
1106
|
+
.fill(inspectorVoiceColor)
|
|
1107
|
+
.frame(width: 6, height: 6)
|
|
1108
|
+
Text(inspectorVoiceStateLabel)
|
|
1109
|
+
.font(Typo.mono(8))
|
|
1110
|
+
.foregroundColor(inspectorVoiceColor)
|
|
1111
|
+
Text("V")
|
|
1112
|
+
.font(Typo.monoBold(7))
|
|
1113
|
+
.foregroundColor(Palette.textDim)
|
|
1114
|
+
.padding(.horizontal, 4)
|
|
1115
|
+
.padding(.vertical, 2)
|
|
1116
|
+
.background(
|
|
1117
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1118
|
+
.fill(Palette.surface)
|
|
1119
|
+
.overlay(
|
|
1120
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1121
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1122
|
+
)
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if let transcript = handsOff.lastTranscript, !transcript.isEmpty {
|
|
1127
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
1128
|
+
Text("heard")
|
|
1129
|
+
.font(Typo.mono(7))
|
|
1130
|
+
.foregroundColor(Palette.textMuted)
|
|
1131
|
+
Text(transcript)
|
|
1132
|
+
.font(Typo.mono(8))
|
|
1133
|
+
.foregroundColor(Palette.text)
|
|
1134
|
+
.lineLimit(2)
|
|
1135
|
+
}
|
|
1136
|
+
} else {
|
|
1137
|
+
Text("Voice activity will show up here. Press V to talk.")
|
|
1138
|
+
.font(Typo.mono(8))
|
|
1139
|
+
.foregroundColor(Palette.textMuted)
|
|
1140
|
+
.lineLimit(2)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if let response = handsOff.lastResponse, !response.isEmpty {
|
|
1144
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
1145
|
+
Text("response")
|
|
1146
|
+
.font(Typo.mono(7))
|
|
1147
|
+
.foregroundColor(Palette.textMuted)
|
|
1148
|
+
Text(response)
|
|
1149
|
+
.font(Typo.mono(8))
|
|
1150
|
+
.foregroundColor(Palette.textDim)
|
|
1151
|
+
.lineLimit(2)
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
.padding(.horizontal, 8)
|
|
1156
|
+
.padding(.vertical, 8)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private var inspectorLogTray: some View {
|
|
1160
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
1161
|
+
HStack(spacing: 6) {
|
|
1162
|
+
Text("LOGS")
|
|
1163
|
+
.font(Typo.monoBold(8))
|
|
1164
|
+
.foregroundColor(Palette.textMuted)
|
|
1165
|
+
Spacer()
|
|
1166
|
+
if !visibleDiagnosticEntries.isEmpty {
|
|
1167
|
+
Button("copy") {
|
|
1168
|
+
let text = visibleDiagnosticEntries.map { entry in
|
|
1169
|
+
"\(Self.inspectorLogTimeFormatter.string(from: entry.time)) \(entry.icon) \(entry.message)"
|
|
1170
|
+
}.joined(separator: "\n")
|
|
1171
|
+
NSPasteboard.general.clearContents()
|
|
1172
|
+
NSPasteboard.general.setString(text, forType: .string)
|
|
1173
|
+
controller.flash("Copied logs")
|
|
1174
|
+
}
|
|
1175
|
+
.font(Typo.mono(7))
|
|
1176
|
+
.foregroundColor(Palette.textMuted)
|
|
1177
|
+
.buttonStyle(.plain)
|
|
1178
|
+
}
|
|
1179
|
+
Button("open") {
|
|
1180
|
+
DiagnosticWindow.shared.toggle()
|
|
1181
|
+
}
|
|
1182
|
+
.font(Typo.mono(7))
|
|
1183
|
+
.foregroundColor(Palette.textMuted)
|
|
1184
|
+
.buttonStyle(.plain)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if visibleDiagnosticEntries.isEmpty {
|
|
1188
|
+
Text("Waiting for diagnostic activity.")
|
|
1189
|
+
.font(Typo.mono(8))
|
|
1190
|
+
.foregroundColor(Palette.textMuted)
|
|
1191
|
+
} else {
|
|
1192
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
1193
|
+
ForEach(visibleDiagnosticEntries) { entry in
|
|
1194
|
+
HStack(alignment: .top, spacing: 6) {
|
|
1195
|
+
Text(Self.inspectorLogTimeFormatter.string(from: entry.time))
|
|
1196
|
+
.font(Typo.mono(7))
|
|
1197
|
+
.foregroundColor(Palette.textMuted)
|
|
1198
|
+
.frame(width: 52, alignment: .leading)
|
|
1199
|
+
Text(entry.icon)
|
|
1200
|
+
.font(Typo.monoBold(7))
|
|
1201
|
+
.foregroundColor(inspectorLogColor(entry.level))
|
|
1202
|
+
.frame(width: 8, alignment: .leading)
|
|
1203
|
+
Text(entry.message)
|
|
1204
|
+
.font(Typo.mono(8))
|
|
1205
|
+
.foregroundColor(inspectorLogColor(entry.level))
|
|
1206
|
+
.lineLimit(2)
|
|
1207
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
.padding(.horizontal, 8)
|
|
1215
|
+
.padding(.vertical, 8)
|
|
1216
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private func inspectorLogColor(_ level: DiagnosticLog.Entry.Level) -> Color {
|
|
1220
|
+
switch level {
|
|
1221
|
+
case .info: return Palette.textDim
|
|
1222
|
+
case .success: return Palette.running
|
|
1223
|
+
case .warning: return Palette.detach
|
|
1224
|
+
case .error: return Palette.kill
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
857
1228
|
private func inspectorRow(label: String, value: String) -> some View {
|
|
858
1229
|
HStack(alignment: .top, spacing: 0) {
|
|
859
1230
|
Text(label)
|
|
@@ -891,6 +1262,13 @@ struct ScreenMapView: View {
|
|
|
891
1262
|
.font(Typo.mono(9))
|
|
892
1263
|
.foregroundColor(Palette.textDim)
|
|
893
1264
|
|
|
1265
|
+
Text("·")
|
|
1266
|
+
.foregroundColor(Palette.textMuted)
|
|
1267
|
+
|
|
1268
|
+
Text(editor.viewportPresetSummary.uppercased())
|
|
1269
|
+
.font(Typo.monoBold(8))
|
|
1270
|
+
.foregroundColor(Palette.textMuted)
|
|
1271
|
+
|
|
894
1272
|
if let focused = editor.focusedDisplay {
|
|
895
1273
|
Text("·")
|
|
896
1274
|
.foregroundColor(Palette.textMuted)
|
|
@@ -1095,6 +1473,10 @@ struct ScreenMapView: View {
|
|
|
1095
1473
|
}
|
|
1096
1474
|
.coordinateSpace(name: "layerSidebar")
|
|
1097
1475
|
|
|
1476
|
+
Spacer(minLength: 8)
|
|
1477
|
+
windowSetsSection(editor: editor)
|
|
1478
|
+
Spacer(minLength: 8)
|
|
1479
|
+
canvasExplorer(editor: editor)
|
|
1098
1480
|
Spacer(minLength: 8)
|
|
1099
1481
|
sidebarMiniMap(editor: editor)
|
|
1100
1482
|
}
|
|
@@ -1112,6 +1494,119 @@ struct ScreenMapView: View {
|
|
|
1112
1494
|
return wins.sorted { $0.zIndex < $1.zIndex }
|
|
1113
1495
|
}
|
|
1114
1496
|
|
|
1497
|
+
private func windowSetsSection(editor: ScreenMapEditorState) -> some View {
|
|
1498
|
+
let sets = controller.windowSets
|
|
1499
|
+
let canSave = !controller.selectedWindowIds.isEmpty
|
|
1500
|
+
|
|
1501
|
+
return VStack(alignment: .leading, spacing: 4) {
|
|
1502
|
+
HStack(spacing: 6) {
|
|
1503
|
+
Text("SETS")
|
|
1504
|
+
.font(Typo.monoBold(8))
|
|
1505
|
+
.foregroundColor(Palette.textMuted)
|
|
1506
|
+
Text("\(sets.count)")
|
|
1507
|
+
.font(Typo.mono(7))
|
|
1508
|
+
.foregroundColor(Palette.textDim)
|
|
1509
|
+
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
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if sets.isEmpty {
|
|
1532
|
+
Text("Save a selection to create a reusable cluster.")
|
|
1533
|
+
.font(Typo.mono(7))
|
|
1534
|
+
.foregroundColor(Palette.textMuted)
|
|
1535
|
+
.padding(.top, 2)
|
|
1536
|
+
} else {
|
|
1537
|
+
ForEach(sets) { set in
|
|
1538
|
+
windowSetRow(set: set, editor: editor)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
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
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private func windowSetRow(set: ScreenMapWindowSet, editor: ScreenMapEditorState) -> some View {
|
|
1554
|
+
let liveCount = editor.windows(matching: set.windowIds).count
|
|
1555
|
+
let isActive = controller.activeWindowSetID == set.id
|
|
1556
|
+
|
|
1557
|
+
return HStack(spacing: 6) {
|
|
1558
|
+
Button {
|
|
1559
|
+
controller.focusWindowSet(set)
|
|
1560
|
+
} label: {
|
|
1561
|
+
HStack(spacing: 6) {
|
|
1562
|
+
Circle()
|
|
1563
|
+
.fill(isActive ? Self.shelfGreen : Palette.textMuted.opacity(0.7))
|
|
1564
|
+
.frame(width: 6, height: 6)
|
|
1565
|
+
VStack(alignment: .leading, spacing: 1) {
|
|
1566
|
+
Text(set.name)
|
|
1567
|
+
.font(Typo.monoBold(8))
|
|
1568
|
+
.foregroundColor(isActive ? Palette.text : Palette.textDim)
|
|
1569
|
+
.lineLimit(1)
|
|
1570
|
+
Text("\(liveCount) window\(liveCount == 1 ? "" : "s")")
|
|
1571
|
+
.font(Typo.mono(7))
|
|
1572
|
+
.foregroundColor(Palette.textMuted)
|
|
1573
|
+
.lineLimit(1)
|
|
1574
|
+
}
|
|
1575
|
+
Spacer()
|
|
1576
|
+
}
|
|
1577
|
+
.padding(.horizontal, 6)
|
|
1578
|
+
.padding(.vertical, 4)
|
|
1579
|
+
.background(
|
|
1580
|
+
RoundedRectangle(cornerRadius: 4)
|
|
1581
|
+
.fill(isActive ? Self.shelfGreen.opacity(0.08) : Palette.surface.opacity(0.35))
|
|
1582
|
+
.overlay(
|
|
1583
|
+
RoundedRectangle(cornerRadius: 4)
|
|
1584
|
+
.strokeBorder(isActive ? Self.shelfGreen.opacity(0.2) : Color.white.opacity(0.04), lineWidth: 0.5)
|
|
1585
|
+
)
|
|
1586
|
+
)
|
|
1587
|
+
}
|
|
1588
|
+
.buttonStyle(.plain)
|
|
1589
|
+
|
|
1590
|
+
Button {
|
|
1591
|
+
controller.deleteWindowSet(set)
|
|
1592
|
+
} label: {
|
|
1593
|
+
Image(systemName: "xmark")
|
|
1594
|
+
.font(.system(size: 7, weight: .bold))
|
|
1595
|
+
.foregroundColor(Palette.textMuted)
|
|
1596
|
+
.frame(width: 16, height: 16)
|
|
1597
|
+
.background(
|
|
1598
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1599
|
+
.fill(Palette.surface.opacity(0.8))
|
|
1600
|
+
.overlay(
|
|
1601
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1602
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1603
|
+
)
|
|
1604
|
+
)
|
|
1605
|
+
}
|
|
1606
|
+
.buttonStyle(.plain)
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1115
1610
|
private func layerTreeHeader(label: String, count: Int, isActive: Bool, color: Color,
|
|
1116
1611
|
isExpandable: Bool = false, isExpanded: Bool = false,
|
|
1117
1612
|
onToggleExpand: (() -> Void)? = nil,
|
|
@@ -1165,8 +1660,8 @@ struct ScreenMapView: View {
|
|
|
1165
1660
|
|
|
1166
1661
|
let bboxPad: CGFloat = (!isFocused && displays.count > 1) ? 40 : 0
|
|
1167
1662
|
let bbox: CGRect = {
|
|
1168
|
-
if let
|
|
1169
|
-
return
|
|
1663
|
+
if let editor {
|
|
1664
|
+
return editor.canvasWorldBounds
|
|
1170
1665
|
}
|
|
1171
1666
|
guard !displays.isEmpty else {
|
|
1172
1667
|
let s = NSScreen.main?.frame ?? CGRect(x: 0, y: 0, width: 1920, height: 1080)
|
|
@@ -1220,10 +1715,11 @@ struct ScreenMapView: View {
|
|
|
1220
1715
|
.frame(width: mapW, height: mapH)
|
|
1221
1716
|
.offset(x: centerX + panOffset.x, y: centerY + panOffset.y)
|
|
1222
1717
|
.onAppear {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1718
|
+
syncCanvasGeometry(editor: editor, fitScale: fitScale, scale: effScale,
|
|
1719
|
+
offsetX: centerX, offsetY: centerY,
|
|
1720
|
+
viewportSize: CGSize(width: max(geo.size.width - 16, 1), height: max(geo.size.height - 16, 1)),
|
|
1721
|
+
screenSize: CGSize(width: screenW, height: screenH),
|
|
1722
|
+
bboxOrigin: bboxOriginPt)
|
|
1227
1723
|
}
|
|
1228
1724
|
.onChange(of: geo.size) { _ in
|
|
1229
1725
|
let newFitScale = min((geo.size.width - 24) / screenW, (geo.size.height - 16) / screenH)
|
|
@@ -1232,10 +1728,32 @@ struct ScreenMapView: View {
|
|
|
1232
1728
|
let newMapH = screenH * newEffScale
|
|
1233
1729
|
let newCX = (geo.size.width - newMapW) / 2
|
|
1234
1730
|
let newCY = (geo.size.height - newMapH) / 2
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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)
|
|
1239
1757
|
}
|
|
1240
1758
|
}
|
|
1241
1759
|
.padding(8)
|
|
@@ -1371,7 +1889,7 @@ struct ScreenMapView: View {
|
|
|
1371
1889
|
|
|
1372
1890
|
@ViewBuilder
|
|
1373
1891
|
private func windowTile(win: ScreenMapWindowEntry, editor: ScreenMapEditorState?, scale: CGFloat, bboxOrigin: CGPoint = .zero) -> some View {
|
|
1374
|
-
let f = win.
|
|
1892
|
+
let f = win.virtualFrame
|
|
1375
1893
|
let x = (f.origin.x - bboxOrigin.x) * scale
|
|
1376
1894
|
let y = (f.origin.y - bboxOrigin.y) * scale
|
|
1377
1895
|
let w = max(f.width * scale, 4)
|
|
@@ -1426,7 +1944,7 @@ struct ScreenMapView: View {
|
|
|
1426
1944
|
.lineLimit(1)
|
|
1427
1945
|
}
|
|
1428
1946
|
if h > 50 {
|
|
1429
|
-
Text("\(Int(win.
|
|
1947
|
+
Text("\(Int(win.virtualFrame.width))x\(Int(win.virtualFrame.height))")
|
|
1430
1948
|
.font(Typo.mono(6))
|
|
1431
1949
|
.foregroundColor(Palette.textMuted)
|
|
1432
1950
|
}
|
|
@@ -1527,13 +2045,51 @@ struct ScreenMapView: View {
|
|
|
1527
2045
|
.allowsHitTesting(false)
|
|
1528
2046
|
}
|
|
1529
2047
|
|
|
1530
|
-
// MARK: - Canvas
|
|
2048
|
+
// MARK: - Canvas Viewport Controls
|
|
2049
|
+
|
|
2050
|
+
private func canvasViewportDock(editor: ScreenMapEditorState) -> some View {
|
|
2051
|
+
VStack(alignment: .trailing, spacing: 6) {
|
|
2052
|
+
HStack(spacing: 4) {
|
|
2053
|
+
ForEach(ScreenMapViewportPreset.allCases) { preset in
|
|
2054
|
+
canvasViewportPresetPill(preset, isActive: editor.activeViewportPreset == preset)
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
canvasZoomControls(editor: editor)
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
private func canvasViewportPresetPill(_ preset: ScreenMapViewportPreset, isActive: Bool) -> some View {
|
|
2062
|
+
Button {
|
|
2063
|
+
controller.focusViewportPreset(preset)
|
|
2064
|
+
} label: {
|
|
2065
|
+
HStack(spacing: 4) {
|
|
2066
|
+
Text(preset.keyHint)
|
|
2067
|
+
.font(Typo.monoBold(8))
|
|
2068
|
+
.foregroundColor(isActive ? Color.black : Palette.textDim)
|
|
2069
|
+
Text(preset.shortLabel)
|
|
2070
|
+
.font(Typo.monoBold(8))
|
|
2071
|
+
.foregroundColor(isActive ? Color.black : Palette.text)
|
|
2072
|
+
}
|
|
2073
|
+
.padding(.horizontal, 7)
|
|
2074
|
+
.padding(.vertical, 4)
|
|
2075
|
+
.background(
|
|
2076
|
+
RoundedRectangle(cornerRadius: 5)
|
|
2077
|
+
.fill(isActive ? Self.shelfGreen.opacity(0.95) : Color(red: 0.1, green: 0.1, blue: 0.11).opacity(0.88))
|
|
2078
|
+
.overlay(
|
|
2079
|
+
RoundedRectangle(cornerRadius: 5)
|
|
2080
|
+
.strokeBorder(isActive ? Self.shelfGreen.opacity(0.95) : Palette.border, lineWidth: 0.5)
|
|
2081
|
+
)
|
|
2082
|
+
)
|
|
2083
|
+
}
|
|
2084
|
+
.buttonStyle(.plain)
|
|
2085
|
+
}
|
|
1531
2086
|
|
|
1532
2087
|
private func canvasZoomControls(editor: ScreenMapEditorState) -> some View {
|
|
1533
2088
|
let pct = Int(editor.zoomLevel * 100)
|
|
1534
2089
|
return HStack(spacing: 0) {
|
|
1535
2090
|
Button {
|
|
1536
2091
|
let newZoom = max(ScreenMapEditorState.minZoom, editor.zoomLevel - 0.25)
|
|
2092
|
+
editor.activeViewportPreset = nil
|
|
1537
2093
|
editor.zoomLevel = newZoom
|
|
1538
2094
|
editor.objectWillChange.send()
|
|
1539
2095
|
controller.objectWillChange.send()
|
|
@@ -1548,9 +2104,7 @@ struct ScreenMapView: View {
|
|
|
1548
2104
|
Rectangle().fill(Palette.border).frame(width: 0.5, height: 12)
|
|
1549
2105
|
|
|
1550
2106
|
Button {
|
|
1551
|
-
|
|
1552
|
-
controller.flash("Fit all")
|
|
1553
|
-
controller.objectWillChange.send()
|
|
2107
|
+
controller.focusViewportPreset(.overview)
|
|
1554
2108
|
} label: {
|
|
1555
2109
|
Text("\(pct)%")
|
|
1556
2110
|
.font(Typo.mono(9))
|
|
@@ -1563,6 +2117,7 @@ struct ScreenMapView: View {
|
|
|
1563
2117
|
|
|
1564
2118
|
Button {
|
|
1565
2119
|
let newZoom = min(ScreenMapEditorState.maxZoom, editor.zoomLevel + 0.25)
|
|
2120
|
+
editor.activeViewportPreset = nil
|
|
1566
2121
|
editor.zoomLevel = newZoom
|
|
1567
2122
|
editor.objectWillChange.send()
|
|
1568
2123
|
controller.objectWillChange.send()
|
|
@@ -1710,6 +2265,11 @@ struct ScreenMapView: View {
|
|
|
1710
2265
|
|
|
1711
2266
|
// Right: docs + logs
|
|
1712
2267
|
HStack(spacing: 10) {
|
|
2268
|
+
statusBarButton(icon: "terminal", label: piChat.isVisible ? "Hide Pi" : "Pi") {
|
|
2269
|
+
withAnimation(.easeOut(duration: 0.16)) {
|
|
2270
|
+
piChat.toggleVisibility()
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
1713
2273
|
statusBarButton(icon: "book", label: "Docs") {
|
|
1714
2274
|
onNavigate?(.docs)
|
|
1715
2275
|
}
|
|
@@ -1765,78 +2325,229 @@ struct ScreenMapView: View {
|
|
|
1765
2325
|
@ViewBuilder
|
|
1766
2326
|
private func sidebarMiniMap(editor: ScreenMapEditorState) -> some View {
|
|
1767
2327
|
let displays = editor.displays
|
|
2328
|
+
let windows = editor.focusedDisplayIndex != nil ? editor.focusedVisibleWindows : editor.visibleWindows
|
|
2329
|
+
let world = editor.canvasWorldBounds
|
|
2330
|
+
let viewport = editor.viewportWorldRect
|
|
2331
|
+
let miniW: CGFloat = sidebarWidth - 28
|
|
2332
|
+
let miniH: CGFloat = 118
|
|
2333
|
+
let scaleW = miniW / max(world.width, 1)
|
|
2334
|
+
let scaleH = miniH / max(world.height, 1)
|
|
2335
|
+
let scale = min(scaleW, scaleH)
|
|
2336
|
+
let drawW = world.width * scale
|
|
2337
|
+
let drawH = world.height * scale
|
|
2338
|
+
let offsetX = (miniW - drawW) / 2
|
|
2339
|
+
let offsetY = (miniH - drawH) / 2
|
|
1768
2340
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
.
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
.buttonStyle(.plain)
|
|
1810
|
-
.offset(x: dx + inset, y: dy + inset)
|
|
1811
|
-
}
|
|
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
|
+
}
|
|
2351
|
+
|
|
2352
|
+
ZStack(alignment: .topLeading) {
|
|
2353
|
+
RoundedRectangle(cornerRadius: 6)
|
|
2354
|
+
.fill(Color.black.opacity(0.28))
|
|
2355
|
+
|
|
2356
|
+
ZStack(alignment: .topLeading) {
|
|
2357
|
+
RoundedRectangle(cornerRadius: 5)
|
|
2358
|
+
.fill(Palette.bg.opacity(0.35))
|
|
2359
|
+
.frame(width: drawW, height: drawH)
|
|
2360
|
+
.offset(x: offsetX, y: offsetY)
|
|
2361
|
+
|
|
2362
|
+
ForEach(displays, id: \.index) { disp in
|
|
2363
|
+
let dx = (disp.cgRect.origin.x - world.origin.x) * scale + offsetX
|
|
2364
|
+
let dy = (disp.cgRect.origin.y - world.origin.y) * scale + offsetY
|
|
2365
|
+
let dw = disp.cgRect.width * scale
|
|
2366
|
+
let dh = disp.cgRect.height * scale
|
|
2367
|
+
let isFocused = editor.focusedDisplayIndex == nil || editor.focusedDisplayIndex == disp.index
|
|
2368
|
+
|
|
2369
|
+
RoundedRectangle(cornerRadius: 3)
|
|
2370
|
+
.fill(isFocused ? Color.white.opacity(0.05) : Color.white.opacity(0.02))
|
|
2371
|
+
.overlay(
|
|
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)
|
|
1812
2380
|
}
|
|
1813
|
-
|
|
2381
|
+
|
|
2382
|
+
ForEach(Array(windows.sorted(by: { $0.zIndex > $1.zIndex }).enumerated()), id: \.element.id) { _, win in
|
|
2383
|
+
let rect = win.virtualFrame
|
|
2384
|
+
let x = (rect.origin.x - world.origin.x) * scale + offsetX
|
|
2385
|
+
let y = (rect.origin.y - world.origin.y) * scale + offsetY
|
|
2386
|
+
let w = max(rect.width * scale, 2)
|
|
2387
|
+
let h = max(rect.height * scale, 2)
|
|
2388
|
+
let isSelected = controller.selectedWindowIds.contains(win.id)
|
|
2389
|
+
|
|
2390
|
+
RoundedRectangle(cornerRadius: 1.5)
|
|
2391
|
+
.fill((isSelected ? Palette.running : Self.layerColor(for: win.layer)).opacity(isSelected ? 0.35 : 0.18))
|
|
2392
|
+
.overlay(
|
|
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
|
+
}
|
|
2399
|
+
|
|
2400
|
+
let viewportX = (viewport.origin.x - world.origin.x) * scale + offsetX
|
|
2401
|
+
let viewportY = (viewport.origin.y - world.origin.y) * scale + offsetY
|
|
2402
|
+
let viewportW = max(viewport.width * scale, 12)
|
|
2403
|
+
let viewportH = max(viewport.height * scale, 12)
|
|
2404
|
+
|
|
2405
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2406
|
+
.strokeBorder(Palette.running.opacity(0.9), lineWidth: 1.25)
|
|
2407
|
+
.background(
|
|
2408
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2409
|
+
.fill(Palette.running.opacity(0.08))
|
|
2410
|
+
)
|
|
2411
|
+
.frame(width: viewportW, height: viewportH)
|
|
2412
|
+
.offset(x: viewportX, y: viewportY)
|
|
1814
2413
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
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)
|
|
2428
|
+
}
|
|
2429
|
+
)
|
|
1817
2430
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
controller.
|
|
1821
|
-
} label: {
|
|
1822
|
-
Text("ALL")
|
|
1823
|
-
.font(Typo.monoBold(7))
|
|
1824
|
-
.foregroundColor(editor.focusedDisplayIndex == nil ? Palette.running : Palette.textDim)
|
|
1825
|
-
.frame(maxWidth: .infinity)
|
|
1826
|
-
.padding(.vertical, 2)
|
|
2431
|
+
HStack(spacing: 6) {
|
|
2432
|
+
mapScopePill("ALL", isActive: editor.focusedDisplayIndex == nil) {
|
|
2433
|
+
controller.focusCanvas(on: editor.canvasWorldBounds, focusDisplay: nil, zoomToFit: true)
|
|
1827
2434
|
}
|
|
1828
|
-
|
|
2435
|
+
ForEach(editor.spatialDisplayOrder, id: \.index) { disp in
|
|
2436
|
+
mapScopePill("\(editor.spatialNumber(for: disp.index))", isActive: editor.focusedDisplayIndex == disp.index) {
|
|
2437
|
+
controller.focusCanvas(
|
|
2438
|
+
on: editor.canvasExplorerRegions.first(where: { $0.kind == .display && $0.displayIndex == disp.index })?.rect ?? disp.cgRect,
|
|
2439
|
+
focusDisplay: disp.index,
|
|
2440
|
+
zoomToFit: true
|
|
2441
|
+
)
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
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
|
+
}
|
|
2456
|
+
|
|
2457
|
+
private func canvasExplorer(editor: ScreenMapEditorState) -> some View {
|
|
2458
|
+
let regions = editor.canvasExplorerRegions
|
|
2459
|
+
|
|
2460
|
+
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
|
+
ForEach(regions.prefix(8)) { region in
|
|
2474
|
+
canvasExplorerRow(region: region)
|
|
1829
2475
|
}
|
|
1830
|
-
|
|
2476
|
+
}
|
|
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
|
+
}
|
|
2487
|
+
|
|
2488
|
+
private func canvasExplorerRow(region: ScreenMapCanvasRegion) -> some View {
|
|
2489
|
+
let tint: Color = {
|
|
2490
|
+
switch region.kind {
|
|
2491
|
+
case .overview: return Palette.running
|
|
2492
|
+
case .display: return Color.blue.opacity(0.8)
|
|
2493
|
+
case .layer: return Self.layerColor(for: region.layer ?? 0)
|
|
2494
|
+
}
|
|
2495
|
+
}()
|
|
2496
|
+
|
|
2497
|
+
return Button {
|
|
2498
|
+
controller.jumpToCanvasRegion(region)
|
|
2499
|
+
controller.flash(region.title)
|
|
2500
|
+
} label: {
|
|
2501
|
+
HStack(spacing: 6) {
|
|
2502
|
+
Circle()
|
|
2503
|
+
.fill(tint)
|
|
2504
|
+
.frame(width: 6, height: 6)
|
|
2505
|
+
VStack(alignment: .leading, spacing: 1) {
|
|
2506
|
+
Text(region.title)
|
|
2507
|
+
.font(Typo.monoBold(8))
|
|
2508
|
+
.foregroundColor(Palette.text)
|
|
2509
|
+
.lineLimit(1)
|
|
2510
|
+
Text(region.subtitle)
|
|
2511
|
+
.font(Typo.mono(7))
|
|
2512
|
+
.foregroundColor(Palette.textMuted)
|
|
2513
|
+
.lineLimit(1)
|
|
2514
|
+
}
|
|
2515
|
+
Spacer()
|
|
2516
|
+
Text("\(region.count)")
|
|
2517
|
+
.font(Typo.mono(7))
|
|
2518
|
+
.foregroundColor(Palette.textDim)
|
|
2519
|
+
}
|
|
2520
|
+
.padding(.horizontal, 6)
|
|
2521
|
+
.padding(.vertical, 4)
|
|
1831
2522
|
.background(
|
|
1832
|
-
RoundedRectangle(cornerRadius:
|
|
1833
|
-
.fill(
|
|
2523
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2524
|
+
.fill(tint.opacity(0.08))
|
|
1834
2525
|
.overlay(
|
|
1835
|
-
RoundedRectangle(cornerRadius:
|
|
1836
|
-
.strokeBorder(
|
|
2526
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2527
|
+
.strokeBorder(tint.opacity(0.18), lineWidth: 0.5)
|
|
1837
2528
|
)
|
|
1838
2529
|
)
|
|
1839
2530
|
}
|
|
2531
|
+
.buttonStyle(.plain)
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
private func mapScopePill(_ label: String, isActive: Bool, action: @escaping () -> Void) -> some View {
|
|
2535
|
+
Button(action: action) {
|
|
2536
|
+
Text(label)
|
|
2537
|
+
.font(Typo.monoBold(7))
|
|
2538
|
+
.foregroundColor(isActive ? Palette.running : Palette.textDim)
|
|
2539
|
+
.padding(.horizontal, 6)
|
|
2540
|
+
.padding(.vertical, 3)
|
|
2541
|
+
.background(
|
|
2542
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2543
|
+
.fill(isActive ? Palette.running.opacity(0.12) : Palette.surface.opacity(0.7))
|
|
2544
|
+
.overlay(
|
|
2545
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2546
|
+
.strokeBorder(isActive ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
|
|
2547
|
+
)
|
|
2548
|
+
)
|
|
2549
|
+
}
|
|
2550
|
+
.buttonStyle(.plain)
|
|
1840
2551
|
}
|
|
1841
2552
|
|
|
1842
2553
|
// MARK: - Flash Overlay
|
|
@@ -1880,14 +2591,17 @@ struct ScreenMapView: View {
|
|
|
1880
2591
|
|
|
1881
2592
|
// MARK: - Helpers
|
|
1882
2593
|
|
|
1883
|
-
private func
|
|
1884
|
-
|
|
1885
|
-
|
|
2594
|
+
private func syncCanvasGeometry(editor: ScreenMapEditorState?, fitScale: CGFloat? = nil, scale: CGFloat,
|
|
2595
|
+
offsetX: CGFloat, offsetY: CGFloat,
|
|
2596
|
+
viewportSize: CGSize,
|
|
2597
|
+
screenSize: CGSize, bboxOrigin: CGPoint = .zero) {
|
|
1886
2598
|
if let fs = fitScale { editor?.fitScale = fs }
|
|
1887
2599
|
editor?.scale = scale
|
|
1888
2600
|
editor?.mapOrigin = CGPoint(x: offsetX, y: offsetY)
|
|
2601
|
+
editor?.viewportSize = viewportSize
|
|
1889
2602
|
editor?.screenSize = screenSize
|
|
1890
2603
|
editor?.bboxOrigin = bboxOrigin
|
|
2604
|
+
controller.applyPendingCanvasNavigationIfNeeded()
|
|
1891
2605
|
}
|
|
1892
2606
|
|
|
1893
2607
|
// MARK: - Layer Colors
|
|
@@ -1896,6 +2610,12 @@ struct ScreenMapView: View {
|
|
|
1896
2610
|
.green, .cyan, .orange, .purple, .pink, .yellow, .mint, .indigo
|
|
1897
2611
|
]
|
|
1898
2612
|
|
|
2613
|
+
private static let inspectorLogTimeFormatter: DateFormatter = {
|
|
2614
|
+
let formatter = DateFormatter()
|
|
2615
|
+
formatter.dateFormat = "HH:mm:ss"
|
|
2616
|
+
return formatter
|
|
2617
|
+
}()
|
|
2618
|
+
|
|
1899
2619
|
private static func layerColor(for layer: Int) -> Color {
|
|
1900
2620
|
layerColors[layer % layerColors.count]
|
|
1901
2621
|
}
|
|
@@ -1904,10 +2624,10 @@ struct ScreenMapView: View {
|
|
|
1904
2624
|
guard let disp = displays.first(where: { $0.index == win.displayIndex }) else { return nil }
|
|
1905
2625
|
let screenW = disp.cgRect.width
|
|
1906
2626
|
let screenH = disp.cgRect.height
|
|
1907
|
-
let relX = win.
|
|
1908
|
-
let relY = win.
|
|
1909
|
-
let winW = win.
|
|
1910
|
-
let winH = win.
|
|
2627
|
+
let relX = win.virtualFrame.origin.x - disp.cgRect.origin.x
|
|
2628
|
+
let relY = win.virtualFrame.origin.y - disp.cgRect.origin.y
|
|
2629
|
+
let winW = win.virtualFrame.width
|
|
2630
|
+
let winH = win.virtualFrame.height
|
|
1911
2631
|
let tolerance: CGFloat = 30
|
|
1912
2632
|
|
|
1913
2633
|
for pos in TilePosition.allCases {
|
|
@@ -1975,6 +2695,9 @@ struct ScreenMapView: View {
|
|
|
1975
2695
|
// Only handle keys when our window is the key window
|
|
1976
2696
|
guard let win = ScreenMapWindowController.shared.nsWindow,
|
|
1977
2697
|
win.isKeyWindow else { return event }
|
|
2698
|
+
if isEditableTextResponder(win.firstResponder) {
|
|
2699
|
+
return event
|
|
2700
|
+
}
|
|
1978
2701
|
// Track space key for canvas drag-to-pan
|
|
1979
2702
|
if event.keyCode == 49 && !controller.isSearchActive {
|
|
1980
2703
|
if event.type == .keyDown && !event.isARepeat {
|
|
@@ -1994,6 +2717,20 @@ struct ScreenMapView: View {
|
|
|
1994
2717
|
}
|
|
1995
2718
|
}
|
|
1996
2719
|
|
|
2720
|
+
private func isEditableTextResponder(_ responder: NSResponder?) -> Bool {
|
|
2721
|
+
if let textView = responder as? NSTextView {
|
|
2722
|
+
return textView.isEditable || textView.isFieldEditor
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
if let textField = responder as? NSTextField {
|
|
2726
|
+
return textField.isEditable
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
guard let responder else { return false }
|
|
2730
|
+
let className = NSStringFromClass(type(of: responder))
|
|
2731
|
+
return className.contains("FieldEditor") || className.contains("TextView")
|
|
2732
|
+
}
|
|
2733
|
+
|
|
1997
2734
|
private func removeKeyHandler() {
|
|
1998
2735
|
if let monitor = eventMonitor {
|
|
1999
2736
|
NSEvent.removeMonitor(monitor)
|
|
@@ -2038,6 +2775,7 @@ struct ScreenMapView: View {
|
|
|
2038
2775
|
if isSpaceHeld, let start = spaceDragStart, let editor = controller.editor {
|
|
2039
2776
|
let dx = event.locationInWindow.x - start.x
|
|
2040
2777
|
let dy = event.locationInWindow.y - start.y
|
|
2778
|
+
editor.activeViewportPreset = nil
|
|
2041
2779
|
editor.panOffset = CGPoint(x: spaceDragPanStart.x + dx, y: spaceDragPanStart.y - dy)
|
|
2042
2780
|
editor.objectWillChange.send()
|
|
2043
2781
|
controller.objectWillChange.send()
|
|
@@ -2053,7 +2791,7 @@ struct ScreenMapView: View {
|
|
|
2053
2791
|
if editor.draggingWindowId != hitId {
|
|
2054
2792
|
editor.draggingWindowId = hitId
|
|
2055
2793
|
if let idx = editor.windows.firstIndex(where: { $0.id == hitId }) {
|
|
2056
|
-
editor.dragStartFrame = editor.windows[idx].
|
|
2794
|
+
editor.dragStartFrame = editor.windows[idx].virtualFrame
|
|
2057
2795
|
}
|
|
2058
2796
|
controller.selectSingle(hitId)
|
|
2059
2797
|
}
|
|
@@ -2110,7 +2848,7 @@ struct ScreenMapView: View {
|
|
|
2110
2848
|
newFrame.size.height = max(minH, startFrame.height + screenDy)
|
|
2111
2849
|
}
|
|
2112
2850
|
|
|
2113
|
-
editor.
|
|
2851
|
+
editor.syncLayoutFrame(at: idx, to: newFrame)
|
|
2114
2852
|
editor.objectWillChange.send()
|
|
2115
2853
|
controller.objectWillChange.send()
|
|
2116
2854
|
return nil
|
|
@@ -2197,11 +2935,13 @@ struct ScreenMapView: View {
|
|
|
2197
2935
|
let newPanX = cursorFromCenter.x - ratio * (cursorFromCenter.x - editor.panOffset.x)
|
|
2198
2936
|
let newPanY = cursorFromCenter.y - ratio * (cursorFromCenter.y - editor.panOffset.y)
|
|
2199
2937
|
|
|
2938
|
+
editor.activeViewportPreset = nil
|
|
2200
2939
|
editor.zoomLevel = newZoom
|
|
2201
2940
|
editor.panOffset = CGPoint(x: newPanX, y: newPanY)
|
|
2202
2941
|
editor.objectWillChange.send()
|
|
2203
2942
|
controller.objectWillChange.send()
|
|
2204
2943
|
} else {
|
|
2944
|
+
editor.activeViewportPreset = nil
|
|
2205
2945
|
editor.panOffset = CGPoint(
|
|
2206
2946
|
x: editor.panOffset.x + event.scrollingDeltaX,
|
|
2207
2947
|
y: editor.panOffset.y - event.scrollingDeltaY
|
|
@@ -2289,7 +3029,7 @@ struct ScreenMapView: View {
|
|
|
2289
3029
|
let windowPool = editor.focusedDisplayIndex != nil ? editor.focusedVisibleWindows : editor.windows
|
|
2290
3030
|
let sorted = windowPool.sorted(by: { $0.zIndex < $1.zIndex })
|
|
2291
3031
|
for win in sorted {
|
|
2292
|
-
let f = win.
|
|
3032
|
+
let f = win.virtualFrame
|
|
2293
3033
|
let mapRect = CGRect(
|
|
2294
3034
|
x: (f.origin.x - bboxOrig.x) * effScale,
|
|
2295
3035
|
y: (f.origin.y - bboxOrig.y) * effScale,
|
|
@@ -2320,7 +3060,7 @@ struct ScreenMapView: View {
|
|
|
2320
3060
|
let windowPool = editor.focusedDisplayIndex != nil ? editor.focusedVisibleWindows : editor.windows
|
|
2321
3061
|
let sorted = windowPool.sorted(by: { $0.zIndex < $1.zIndex })
|
|
2322
3062
|
for win in sorted {
|
|
2323
|
-
let f = win.
|
|
3063
|
+
let f = win.virtualFrame
|
|
2324
3064
|
let mapRect = CGRect(
|
|
2325
3065
|
x: (f.origin.x - bboxOrig.x) * effScale,
|
|
2326
3066
|
y: (f.origin.y - bboxOrig.y) * effScale,
|
|
@@ -2468,7 +3208,7 @@ struct ScreenMapPreviewOverlay: View {
|
|
|
2468
3208
|
Color.black.opacity(0.88)
|
|
2469
3209
|
|
|
2470
3210
|
ForEach(windows) { win in
|
|
2471
|
-
let f = win.
|
|
3211
|
+
let f = win.virtualFrame
|
|
2472
3212
|
let x = f.origin.x - screenCGOrigin.x
|
|
2473
3213
|
let y = f.origin.y - screenCGOrigin.y
|
|
2474
3214
|
let w = f.width
|