@lattices/cli 0.4.8 → 0.4.10
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/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Sources/AppShell/AppDelegate.swift +4 -0
- package/app/Sources/AppShell/AppUpdater.swift +216 -4
- package/app/Sources/AppShell/SettingsView.swift +78 -3
- package/app/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +38 -19
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/package.json +1 -1
|
@@ -5,8 +5,16 @@ import AppKit
|
|
|
5
5
|
|
|
6
6
|
struct ScreenMapView: View {
|
|
7
7
|
private static let canvasPadding: CGFloat = 8
|
|
8
|
-
private static let canvasFitInsets = CGSize(width:
|
|
8
|
+
private static let canvasFitInsets = CGSize(width: 132, height: 112)
|
|
9
9
|
private static let canvasViewportInsets = CGSize(width: 16, height: 16)
|
|
10
|
+
private static let canvasPanMinVisiblePixels: CGFloat = 1
|
|
11
|
+
private static let canvasFitScaleMultiplier: CGFloat = 0.66
|
|
12
|
+
private static let canvasStageMaxWidth: CGFloat = 980
|
|
13
|
+
private static let canvasStageMaxHeight: CGFloat = 560
|
|
14
|
+
private static let canvasStageMinAspect: CGFloat = 1.25
|
|
15
|
+
private static let canvasStageMaxAspect: CGFloat = 2.45
|
|
16
|
+
private static let sidebarWindowRowHeight: CGFloat = 28
|
|
17
|
+
private static let sidebarWindowRowStride: CGFloat = 30
|
|
10
18
|
|
|
11
19
|
private struct CanvasMetrics: Equatable {
|
|
12
20
|
let worldBounds: CGRect
|
|
@@ -39,7 +47,7 @@ struct ScreenMapView: View {
|
|
|
39
47
|
fitScale = min(
|
|
40
48
|
fitArea.width / max(worldBounds.width, 1),
|
|
41
49
|
fitArea.height / max(worldBounds.height, 1)
|
|
42
|
-
)
|
|
50
|
+
) * ScreenMapView.canvasFitScaleMultiplier
|
|
43
51
|
effectiveScale = fitScale * (editor?.zoomLevel ?? 1)
|
|
44
52
|
mapSize = CGSize(width: worldBounds.width * effectiveScale, height: worldBounds.height * effectiveScale)
|
|
45
53
|
centerOffset = CGPoint(
|
|
@@ -65,7 +73,7 @@ struct ScreenMapView: View {
|
|
|
65
73
|
let panOffset: CGPoint
|
|
66
74
|
|
|
67
75
|
init(editor: ScreenMapEditorState) {
|
|
68
|
-
scale = editor.effectiveScale
|
|
76
|
+
scale = max(editor.fitScale * editor.zoomLevel, editor.effectiveScale)
|
|
69
77
|
bboxOrigin = editor.bboxOrigin
|
|
70
78
|
mapOrigin = editor.mapOrigin
|
|
71
79
|
panOffset = editor.panOffset
|
|
@@ -174,8 +182,8 @@ struct ScreenMapView: View {
|
|
|
174
182
|
@State private var canvasTransitionOffset: CGFloat = 0
|
|
175
183
|
@State private var canvasTransitionOpacity: Double = 1.0
|
|
176
184
|
@State private var isSpaceHeld: Bool = false
|
|
177
|
-
@State private var
|
|
178
|
-
@State private var
|
|
185
|
+
@State private var canvasPanStart: NSPoint? = nil
|
|
186
|
+
@State private var canvasPanStartOffset: CGPoint = .zero
|
|
179
187
|
@State private var searchOverlayFrame: CGRect = .zero
|
|
180
188
|
|
|
181
189
|
var body: some View {
|
|
@@ -416,13 +424,10 @@ struct ScreenMapView: View {
|
|
|
416
424
|
let viewport = editor.viewportWorldRect
|
|
417
425
|
let world = editor.canvasWorldBounds
|
|
418
426
|
let scope = editor.focusedDisplay.map { "\(editor.spatialNumber(for: $0.index)). \($0.label)" } ?? "All Displays"
|
|
419
|
-
let layers = editor.selectedLayers.isEmpty
|
|
420
|
-
? "All Layers"
|
|
421
|
-
: editor.selectedLayers.sorted().map { editor.layerDisplayName(for: $0) }.joined(separator: ", ")
|
|
422
427
|
|
|
423
428
|
return VStack(alignment: .leading, spacing: 4) {
|
|
424
429
|
inspectorRow(label: "Scope", value: scope)
|
|
425
|
-
inspectorRow(label: "
|
|
430
|
+
inspectorRow(label: "Mode", value: "Desktop")
|
|
426
431
|
inspectorRow(label: "View", value: "\(Int(viewport.midX)), \(Int(viewport.midY)) · \(Int(viewport.width))×\(Int(viewport.height))")
|
|
427
432
|
inspectorRow(label: "World", value: "\(Int(world.width))×\(Int(world.height))")
|
|
428
433
|
inspectorRow(label: "Set", value: controller.activeWindowSet?.name ?? "None")
|
|
@@ -444,13 +449,12 @@ struct ScreenMapView: View {
|
|
|
444
449
|
private func inspectorWindowCard(win: ScreenMapWindowEntry, editor: ScreenMapEditorState) -> some View {
|
|
445
450
|
let desktopEntry = DesktopModel.shared.windows[UInt32(win.id)]
|
|
446
451
|
let ocrText = OcrModel.shared.results[UInt32(win.id)]?.fullText
|
|
447
|
-
let layerTag = DesktopModel.shared.windowLayerTags[UInt32(win.id)]
|
|
448
452
|
|
|
449
453
|
return VStack(alignment: .leading, spacing: 8) {
|
|
450
454
|
// Header: app + visibility
|
|
451
455
|
HStack(spacing: 5) {
|
|
452
456
|
Circle()
|
|
453
|
-
.fill(
|
|
457
|
+
.fill(Palette.running.opacity(0.8))
|
|
454
458
|
.frame(width: 6, height: 6)
|
|
455
459
|
Text(win.app)
|
|
456
460
|
.font(Typo.monoBold(11))
|
|
@@ -489,10 +493,6 @@ struct ScreenMapView: View {
|
|
|
489
493
|
|
|
490
494
|
// Layout info
|
|
491
495
|
VStack(alignment: .leading, spacing: 3) {
|
|
492
|
-
inspectorRow(label: "Layer", value: editor.layerDisplayName(for: win.layer))
|
|
493
|
-
if let tag = layerTag {
|
|
494
|
-
inspectorRow(label: "Tag", value: tag)
|
|
495
|
-
}
|
|
496
496
|
inspectorRow(label: "Display", value: {
|
|
497
497
|
if let disp = editor.displays.first(where: { $0.index == win.displayIndex }) {
|
|
498
498
|
return "\(editor.spatialNumber(for: disp.index)). \(disp.label)"
|
|
@@ -603,7 +603,6 @@ struct ScreenMapView: View {
|
|
|
603
603
|
"pid: \(entry.pid)",
|
|
604
604
|
"frame: \(Int(entry.frame.x)),\(Int(entry.frame.y)) \(Int(entry.frame.w))×\(Int(entry.frame.h))",
|
|
605
605
|
entry.latticesSession.map { "session: \($0)" },
|
|
606
|
-
DesktopModel.shared.windowLayerTags[wid].map { "layer: \($0)" },
|
|
607
606
|
].compactMap { $0 }.joined(separator: "\n")
|
|
608
607
|
NSPasteboard.general.clearContents()
|
|
609
608
|
NSPasteboard.general.setString(info, forType: .string)
|
|
@@ -998,8 +997,6 @@ struct ScreenMapView: View {
|
|
|
998
997
|
("g", "grow", { [controller] in controller.fitAvailableSpace() }),
|
|
999
998
|
("u", "set", { [controller] in controller.createWindowSetFromSelection() }),
|
|
1000
999
|
("m", "project", { [controller] in controller.materializeViewport() }),
|
|
1001
|
-
("c", "merge", { [controller] in controller.consolidateLayers() }),
|
|
1002
|
-
("f", "flatten", { [controller] in controller.flattenLayers() }),
|
|
1003
1000
|
("v", "preview", { [controller] in controller.previewLayer() }),
|
|
1004
1001
|
]
|
|
1005
1002
|
|
|
@@ -1434,11 +1431,11 @@ struct ScreenMapView: View {
|
|
|
1434
1431
|
return VStack(spacing: 0) {
|
|
1435
1432
|
// Header
|
|
1436
1433
|
HStack {
|
|
1437
|
-
Text("
|
|
1434
|
+
Text("VIEW")
|
|
1438
1435
|
.font(Typo.monoBold(9))
|
|
1439
1436
|
.foregroundColor(Palette.textMuted)
|
|
1440
1437
|
Spacer()
|
|
1441
|
-
if editor.effectiveLayerCount > 1 {
|
|
1438
|
+
if editor.effectiveLayerCount > 1 && !editor.isShowingAll {
|
|
1442
1439
|
Button(action: { controller.consolidateLayers() }) {
|
|
1443
1440
|
Image(systemName: "arrow.triangle.merge")
|
|
1444
1441
|
.font(.system(size: 8, weight: .semibold))
|
|
@@ -1452,49 +1449,88 @@ struct ScreenMapView: View {
|
|
|
1452
1449
|
|
|
1453
1450
|
// Layer list
|
|
1454
1451
|
ScrollView(.vertical, showsIndicators: false) {
|
|
1455
|
-
let
|
|
1456
|
-
let
|
|
1457
|
-
|
|
1458
|
-
VStack(spacing: 2) {
|
|
1459
|
-
layerTreeHeader(
|
|
1460
|
-
label: "All",
|
|
1461
|
-
count: editor.scopedWindowCount,
|
|
1462
|
-
isActive: editor.isShowingAll,
|
|
1463
|
-
color: Palette.running
|
|
1464
|
-
) {
|
|
1465
|
-
editor.selectLayer(nil)
|
|
1466
|
-
}
|
|
1452
|
+
let visibleWindows = editor.renderedCanvasWindows.sorted { $0.zIndex < $1.zIndex }
|
|
1453
|
+
let rowWidth = max(sidebarWidth - 16, 1)
|
|
1467
1454
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1455
|
+
ZStack(alignment: .topLeading) {
|
|
1456
|
+
VStack(spacing: 2) {
|
|
1457
|
+
layerTreeHeader(
|
|
1458
|
+
label: "Desktop",
|
|
1459
|
+
count: visibleWindows.count,
|
|
1460
|
+
isActive: editor.isShowingAll,
|
|
1461
|
+
color: Palette.running
|
|
1462
|
+
) {
|
|
1463
|
+
editor.selectLayer(nil)
|
|
1464
|
+
}
|
|
1465
|
+
.frame(width: rowWidth, height: Self.sidebarWindowRowHeight, alignment: .leading)
|
|
1471
1466
|
|
|
1472
|
-
|
|
1473
|
-
|
|
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()
|
|
1467
|
+
ForEach(visibleWindows) { win in
|
|
1468
|
+
visibleWindowRow(win: win)
|
|
1485
1469
|
}
|
|
1486
|
-
.padding(.vertical, 4)
|
|
1487
|
-
.padding(.horizontal, 4)
|
|
1488
|
-
.contentShape(Rectangle())
|
|
1489
|
-
.simultaneousGesture(TapGesture().onEnded { showUnnamedLayers.toggle() })
|
|
1490
1470
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1471
|
+
/*
|
|
1472
|
+
Depth controls intentionally stay out of the default flow.
|
|
1473
|
+
Keep the old layer model available internally, but make the
|
|
1474
|
+
normal sidebar answer: "which windows are visible right now?"
|
|
1475
|
+
*/
|
|
1476
|
+
if !editor.isShowingAll {
|
|
1477
|
+
let namedLayers = editor.namedEffectiveLayers
|
|
1478
|
+
let unnamedLayers = editor.unnamedEffectiveLayers
|
|
1479
|
+
ForEach(namedLayers, id: \.self) { layer in
|
|
1493
1480
|
layerRow(layer: layer, editor: editor)
|
|
1494
1481
|
}
|
|
1482
|
+
|
|
1483
|
+
if !unnamedLayers.isEmpty {
|
|
1484
|
+
HStack(spacing: 4) {
|
|
1485
|
+
let totalWindows = unnamedLayers.reduce(0) { $0 + editor.layerTreeWindows(for: $1).count }
|
|
1486
|
+
Image(systemName: showUnnamedLayers ? "chevron.down" : "chevron.right")
|
|
1487
|
+
.font(.system(size: 6, weight: .bold))
|
|
1488
|
+
.foregroundColor(Palette.textMuted)
|
|
1489
|
+
Text(showUnnamedLayers ? "hide depth" : "show depth")
|
|
1490
|
+
.font(Typo.mono(8))
|
|
1491
|
+
.foregroundColor(Palette.textMuted)
|
|
1492
|
+
Text("· \(totalWindows)w")
|
|
1493
|
+
.font(Typo.mono(7))
|
|
1494
|
+
.foregroundColor(Palette.textDim)
|
|
1495
|
+
Spacer()
|
|
1496
|
+
}
|
|
1497
|
+
.padding(.vertical, 4)
|
|
1498
|
+
.padding(.horizontal, 4)
|
|
1499
|
+
.contentShape(Rectangle())
|
|
1500
|
+
.simultaneousGesture(TapGesture().onEnded { showUnnamedLayers.toggle() })
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if showUnnamedLayers {
|
|
1504
|
+
ForEach(unnamedLayers, id: \.self) { layer in
|
|
1505
|
+
layerRow(layer: layer, editor: editor)
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1495
1508
|
}
|
|
1496
1509
|
}
|
|
1510
|
+
.frame(width: rowWidth, alignment: .topLeading)
|
|
1511
|
+
.allowsHitTesting(!editor.isShowingAll)
|
|
1512
|
+
|
|
1513
|
+
if editor.isShowingAll {
|
|
1514
|
+
SidebarWindowHitCatcher(rowHeight: Self.sidebarWindowRowStride) { row in
|
|
1515
|
+
if row == 0 {
|
|
1516
|
+
editor.selectLayer(nil)
|
|
1517
|
+
} else {
|
|
1518
|
+
let index = row - 1
|
|
1519
|
+
guard visibleWindows.indices.contains(index) else { return }
|
|
1520
|
+
controller.selectSingle(visibleWindows[index].id)
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
.frame(
|
|
1524
|
+
width: rowWidth,
|
|
1525
|
+
height: CGFloat(visibleWindows.count + 1) * Self.sidebarWindowRowStride
|
|
1526
|
+
)
|
|
1527
|
+
}
|
|
1497
1528
|
}
|
|
1529
|
+
.frame(
|
|
1530
|
+
width: rowWidth,
|
|
1531
|
+
height: CGFloat(visibleWindows.count + 1) * Self.sidebarWindowRowStride,
|
|
1532
|
+
alignment: .topLeading
|
|
1533
|
+
)
|
|
1498
1534
|
}
|
|
1499
1535
|
.coordinateSpace(name: "layerSidebar")
|
|
1500
1536
|
|
|
@@ -1514,6 +1550,50 @@ struct ScreenMapView: View {
|
|
|
1514
1550
|
.onPreferenceChange(LayerRowFrameKey.self) { layerRowFrames = $0 }
|
|
1515
1551
|
}
|
|
1516
1552
|
|
|
1553
|
+
private func visibleWindowRow(win: ScreenMapWindowEntry) -> some View {
|
|
1554
|
+
let isSelected = controller.selectedWindowIds.contains(win.id)
|
|
1555
|
+
let rowWidth = max(sidebarWidth - 16, 1)
|
|
1556
|
+
return HStack(spacing: 6) {
|
|
1557
|
+
Circle()
|
|
1558
|
+
.fill(isSelected ? Palette.running : Palette.textMuted.opacity(0.55))
|
|
1559
|
+
.frame(width: 4, height: 4)
|
|
1560
|
+
VStack(alignment: .leading, spacing: 1) {
|
|
1561
|
+
Text(win.app)
|
|
1562
|
+
.font(Typo.monoBold(8))
|
|
1563
|
+
.foregroundColor(isSelected ? Palette.running : Palette.textDim)
|
|
1564
|
+
.lineLimit(1)
|
|
1565
|
+
if !win.title.isEmpty {
|
|
1566
|
+
Text(win.title)
|
|
1567
|
+
.font(Typo.mono(7))
|
|
1568
|
+
.foregroundColor(Palette.textMuted)
|
|
1569
|
+
.lineLimit(1)
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
Spacer(minLength: 4)
|
|
1573
|
+
if win.hasEdits {
|
|
1574
|
+
Circle()
|
|
1575
|
+
.fill(Color.orange.opacity(0.85))
|
|
1576
|
+
.frame(width: 4, height: 4)
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
.padding(.vertical, 3)
|
|
1580
|
+
.padding(.leading, 20)
|
|
1581
|
+
.padding(.trailing, 6)
|
|
1582
|
+
.frame(width: rowWidth, height: Self.sidebarWindowRowHeight, alignment: .leading)
|
|
1583
|
+
.clipped()
|
|
1584
|
+
.background(
|
|
1585
|
+
RoundedRectangle(cornerRadius: 4)
|
|
1586
|
+
.fill(isSelected ? Palette.running.opacity(0.08) : Color.clear)
|
|
1587
|
+
)
|
|
1588
|
+
.contentShape(Rectangle())
|
|
1589
|
+
.highPriorityGesture(TapGesture().onEnded {
|
|
1590
|
+
controller.selectSingle(win.id)
|
|
1591
|
+
})
|
|
1592
|
+
.accessibilityElement(children: .combine)
|
|
1593
|
+
.accessibilityLabel(win.title.isEmpty ? win.app : "\(win.app), \(win.title)")
|
|
1594
|
+
.accessibilityAddTraits(.isButton)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1517
1597
|
@ViewBuilder
|
|
1518
1598
|
private func layerRow(layer: Int, editor: ScreenMapEditorState) -> some View {
|
|
1519
1599
|
let displayName = editor.layerDisplayName(for: layer)
|
|
@@ -1799,6 +1879,22 @@ struct ScreenMapView: View {
|
|
|
1799
1879
|
|
|
1800
1880
|
// MARK: - Canvas
|
|
1801
1881
|
|
|
1882
|
+
private func canvasStageAspectRatio(editor: ScreenMapEditorState?, displays: [DisplayGeometry]) -> CGFloat {
|
|
1883
|
+
let scopedDisplays: [DisplayGeometry]
|
|
1884
|
+
if let focusedDisplayIndex = editor?.focusedDisplayIndex {
|
|
1885
|
+
scopedDisplays = displays.filter { $0.index == focusedDisplayIndex }
|
|
1886
|
+
} else {
|
|
1887
|
+
scopedDisplays = displays
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
let displayBounds = scopedDisplays.map(\.cgRect).reduce(nil as CGRect?) { partial, rect in
|
|
1891
|
+
partial.map { $0.union(rect) } ?? rect
|
|
1892
|
+
}
|
|
1893
|
+
let bounds = displayBounds ?? editor?.canvasWorldBounds ?? CGRect(x: 0, y: 0, width: 16, height: 10)
|
|
1894
|
+
let rawAspect = bounds.width / max(bounds.height, 1)
|
|
1895
|
+
return min(max(rawAspect, Self.canvasStageMinAspect), Self.canvasStageMaxAspect)
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1802
1898
|
private func screenMapCanvas(editor: ScreenMapEditorState?) -> some View {
|
|
1803
1899
|
let isFocused = editor?.focusedDisplayIndex != nil
|
|
1804
1900
|
let canvasWindows = editor?.renderedCanvasWindows ?? []
|
|
@@ -1806,6 +1902,8 @@ struct ScreenMapView: View {
|
|
|
1806
1902
|
let zoomLevel = editor?.zoomLevel ?? 1.0
|
|
1807
1903
|
let panOffset = editor?.panOffset ?? .zero
|
|
1808
1904
|
let canvasShape = RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
1905
|
+
let stageAspectRatio = canvasStageAspectRatio(editor: editor, displays: displays)
|
|
1906
|
+
let usesProjectionWindows = editor?.isShowingAll ?? true
|
|
1809
1907
|
|
|
1810
1908
|
return GeometryReader { geo in
|
|
1811
1909
|
let metrics = CanvasMetrics(editor: editor, displays: displays, viewportSize: geo.size)
|
|
@@ -1815,12 +1913,11 @@ struct ScreenMapView: View {
|
|
|
1815
1913
|
zoomLevel: zoomLevel,
|
|
1816
1914
|
navigationRevision: editor?.canvasNavigationRevision ?? 0
|
|
1817
1915
|
)
|
|
1818
|
-
|
|
1819
1916
|
ZStack(alignment: .topLeading) {
|
|
1820
1917
|
// Per-display background rectangles
|
|
1821
1918
|
if isFocused, editor?.focusedDisplay != nil {
|
|
1822
1919
|
focusedDisplayBackground(mapSize: metrics.mapSize)
|
|
1823
|
-
} else if displays.
|
|
1920
|
+
} else if !displays.isEmpty {
|
|
1824
1921
|
multiDisplayBackgrounds(displays: displays, editor: editor, metrics: metrics)
|
|
1825
1922
|
} else {
|
|
1826
1923
|
singleDisplayBackground(mapSize: metrics.mapSize)
|
|
@@ -1837,9 +1934,11 @@ struct ScreenMapView: View {
|
|
|
1837
1934
|
.offset(x: rect.minX, y: rect.minY)
|
|
1838
1935
|
}
|
|
1839
1936
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1937
|
+
if !usesProjectionWindows {
|
|
1938
|
+
// Live windows back-to-front
|
|
1939
|
+
ForEach(Array(canvasWindows.sorted(by: { $0.zIndex > $1.zIndex }).enumerated()), id: \.element.id) { _, win in
|
|
1940
|
+
windowTile(win: win, editor: editor, metrics: metrics)
|
|
1941
|
+
}
|
|
1843
1942
|
}
|
|
1844
1943
|
}
|
|
1845
1944
|
.frame(width: metrics.mapSize.width, height: metrics.mapSize.height)
|
|
@@ -1852,7 +1951,8 @@ struct ScreenMapView: View {
|
|
|
1852
1951
|
}
|
|
1853
1952
|
}
|
|
1854
1953
|
.padding(8)
|
|
1855
|
-
.frame(maxWidth: .
|
|
1954
|
+
.frame(maxWidth: Self.canvasStageMaxWidth, maxHeight: Self.canvasStageMaxHeight)
|
|
1955
|
+
.aspectRatio(stageAspectRatio, contentMode: .fit)
|
|
1856
1956
|
.contentShape(canvasShape)
|
|
1857
1957
|
.clipShape(canvasShape)
|
|
1858
1958
|
.clipped()
|
|
@@ -1876,6 +1976,85 @@ struct ScreenMapView: View {
|
|
|
1876
1976
|
}
|
|
1877
1977
|
}
|
|
1878
1978
|
)
|
|
1979
|
+
.overlay(alignment: .topLeading) {
|
|
1980
|
+
Group {
|
|
1981
|
+
if let editor = editor {
|
|
1982
|
+
let projection = CanvasProjection(editor: editor)
|
|
1983
|
+
let projectionLabelIds = projectionLabelWindowIds(
|
|
1984
|
+
windows: canvasWindows,
|
|
1985
|
+
projection: projection,
|
|
1986
|
+
selectedIds: controller.selectedWindowIds
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
ZStack(alignment: .topLeading) {
|
|
1990
|
+
if usesProjectionWindows {
|
|
1991
|
+
ForEach(Array(canvasWindows.sorted(by: { $0.zIndex > $1.zIndex }).enumerated()), id: \.element.id) { _, win in
|
|
1992
|
+
let rect = projection.mapRect(for: win.virtualFrame)
|
|
1993
|
+
let radius = min(max(rect.width, rect.height) * 0.012, 3)
|
|
1994
|
+
let isSelected = controller.selectedWindowIds.contains(win.id)
|
|
1995
|
+
let fill = isSelected
|
|
1996
|
+
? Palette.running.opacity(0.10)
|
|
1997
|
+
: win.hasEdits ? Color.orange.opacity(0.10) : Color.white.opacity(0.018)
|
|
1998
|
+
let stroke = isSelected
|
|
1999
|
+
? Palette.running.opacity(0.55)
|
|
2000
|
+
: win.hasEdits ? Color.orange.opacity(0.5) : Color.white.opacity(0.10)
|
|
2001
|
+
|
|
2002
|
+
RoundedRectangle(cornerRadius: radius)
|
|
2003
|
+
.fill(fill)
|
|
2004
|
+
.overlay(
|
|
2005
|
+
RoundedRectangle(cornerRadius: radius)
|
|
2006
|
+
.strokeBorder(stroke, lineWidth: isSelected ? 0.9 : 0.7)
|
|
2007
|
+
)
|
|
2008
|
+
.overlay {
|
|
2009
|
+
if projectionLabelIds.contains(win.id) {
|
|
2010
|
+
projectionWindowLabel(win: win, rect: rect, isSelected: isSelected)
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
.frame(width: rect.width, height: rect.height)
|
|
2014
|
+
.contentShape(Rectangle())
|
|
2015
|
+
.offset(
|
|
2016
|
+
x: Self.canvasPadding + projection.mapOrigin.x + projection.panOffset.x + rect.minX,
|
|
2017
|
+
y: Self.canvasPadding + projection.mapOrigin.y + projection.panOffset.y + rect.minY
|
|
2018
|
+
)
|
|
2019
|
+
.zIndex(isSelected ? 2 : 0)
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
ForEach(editor.displays, id: \.index) { display in
|
|
2024
|
+
let rect = projection.mapRect(for: display.cgRect, minimumSize: 12)
|
|
2025
|
+
let isFocused = editor.focusedDisplayIndex == display.index
|
|
2026
|
+
RoundedRectangle(cornerRadius: 5)
|
|
2027
|
+
.strokeBorder(
|
|
2028
|
+
isFocused ? Palette.running.opacity(0.36) : Color.white.opacity(0.22),
|
|
2029
|
+
lineWidth: isFocused ? 1 : 0.8
|
|
2030
|
+
)
|
|
2031
|
+
.overlay(alignment: .topLeading) {
|
|
2032
|
+
Text("\(editor.spatialNumber(for: display.index))")
|
|
2033
|
+
.font(Typo.monoBold(8))
|
|
2034
|
+
.foregroundColor(isFocused ? Palette.running : Palette.textMuted)
|
|
2035
|
+
.padding(.horizontal, 4)
|
|
2036
|
+
.padding(.vertical, 2)
|
|
2037
|
+
.background(
|
|
2038
|
+
RoundedRectangle(cornerRadius: 3)
|
|
2039
|
+
.fill(Color.black.opacity(0.42))
|
|
2040
|
+
)
|
|
2041
|
+
.padding(5)
|
|
2042
|
+
}
|
|
2043
|
+
.frame(width: rect.width, height: rect.height)
|
|
2044
|
+
.offset(
|
|
2045
|
+
x: Self.canvasPadding + projection.mapOrigin.x + projection.panOffset.x + rect.minX,
|
|
2046
|
+
y: Self.canvasPadding + projection.mapOrigin.y + projection.panOffset.y + rect.minY
|
|
2047
|
+
)
|
|
2048
|
+
.allowsHitTesting(false)
|
|
2049
|
+
.zIndex(-1)
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
.allowsHitTesting(false)
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
.clipShape(canvasShape)
|
|
2057
|
+
.clipped()
|
|
1879
2058
|
.overlay(alignment: .top) {
|
|
1880
2059
|
if let editor = controller.editor, editor.displays.count > 1 {
|
|
1881
2060
|
displayToolbar(editor: editor)
|
|
@@ -1980,6 +2159,108 @@ struct ScreenMapView: View {
|
|
|
1980
2159
|
|
|
1981
2160
|
// MARK: - Window Tile
|
|
1982
2161
|
|
|
2162
|
+
private func projectionLabelWindowIds(
|
|
2163
|
+
windows: [ScreenMapWindowEntry],
|
|
2164
|
+
projection: CanvasProjection,
|
|
2165
|
+
selectedIds: Set<UInt32>
|
|
2166
|
+
) -> Set<UInt32> {
|
|
2167
|
+
if !selectedIds.isEmpty {
|
|
2168
|
+
return Set(windows.compactMap { win in
|
|
2169
|
+
guard selectedIds.contains(win.id) else { return nil }
|
|
2170
|
+
let rect = projection.mapRect(for: win.virtualFrame)
|
|
2171
|
+
return rect.width > 54 && rect.height > 24 ? win.id : nil
|
|
2172
|
+
})
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
var acceptedIds = Set<UInt32>()
|
|
2176
|
+
var occupiedLabelRects: [CGRect] = []
|
|
2177
|
+
|
|
2178
|
+
for (frontOrder, win) in windows.sorted(by: { $0.zIndex < $1.zIndex }).enumerated() {
|
|
2179
|
+
let rect = projection.mapRect(for: win.virtualFrame)
|
|
2180
|
+
let isSelected = selectedIds.contains(win.id)
|
|
2181
|
+
guard isSelected || frontOrder < 14 else { continue }
|
|
2182
|
+
guard rect.width > 54, rect.height > 24 else { continue }
|
|
2183
|
+
|
|
2184
|
+
let labelRect = projectionLabelCollisionRect(for: rect, includesTitle: rect.width > 130 && rect.height > 38 && !win.title.isEmpty)
|
|
2185
|
+
let collides = occupiedLabelRects.contains { occupied in
|
|
2186
|
+
occupied.insetBy(dx: -8, dy: -5).intersects(labelRect)
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if isSelected || !collides {
|
|
2190
|
+
acceptedIds.insert(win.id)
|
|
2191
|
+
occupiedLabelRects.append(labelRect)
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
return acceptedIds
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
private func projectionLabelCollisionRect(for rect: CGRect, includesTitle: Bool) -> CGRect {
|
|
2199
|
+
let width = min(max(48, rect.width - 10), includesTitle ? 170 : 96)
|
|
2200
|
+
let height: CGFloat = includesTitle ? 27 : 15
|
|
2201
|
+
return CGRect(
|
|
2202
|
+
x: rect.midX - width / 2,
|
|
2203
|
+
y: rect.midY - height / 2,
|
|
2204
|
+
width: width,
|
|
2205
|
+
height: height
|
|
2206
|
+
)
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
@ViewBuilder
|
|
2210
|
+
private func projectionWindowLabel(win: ScreenMapWindowEntry, rect: CGRect, isSelected: Bool) -> some View {
|
|
2211
|
+
if rect.width > 54, rect.height > 24 {
|
|
2212
|
+
Group {
|
|
2213
|
+
if isSelected {
|
|
2214
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
2215
|
+
HStack(spacing: 4) {
|
|
2216
|
+
Circle()
|
|
2217
|
+
.fill(Palette.running)
|
|
2218
|
+
.frame(width: 4, height: 4)
|
|
2219
|
+
Text(win.app)
|
|
2220
|
+
.font(Typo.monoBold(9))
|
|
2221
|
+
.foregroundColor(Palette.running)
|
|
2222
|
+
.lineLimit(1)
|
|
2223
|
+
}
|
|
2224
|
+
if rect.width > 120, rect.height > 42, !win.title.isEmpty {
|
|
2225
|
+
Text(win.title)
|
|
2226
|
+
.font(Typo.mono(7))
|
|
2227
|
+
.foregroundColor(Palette.textMuted.opacity(0.86))
|
|
2228
|
+
.lineLimit(1)
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
.padding(.horizontal, 6)
|
|
2232
|
+
.padding(.vertical, 4)
|
|
2233
|
+
.background(
|
|
2234
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2235
|
+
.fill(Color.black.opacity(0.48))
|
|
2236
|
+
)
|
|
2237
|
+
.overlay(
|
|
2238
|
+
RoundedRectangle(cornerRadius: 4)
|
|
2239
|
+
.strokeBorder(Palette.running.opacity(0.42), lineWidth: 0.75)
|
|
2240
|
+
)
|
|
2241
|
+
.frame(maxWidth: rect.width, maxHeight: rect.height)
|
|
2242
|
+
} else {
|
|
2243
|
+
VStack(spacing: 1) {
|
|
2244
|
+
Text(win.app)
|
|
2245
|
+
.font(Typo.monoBold(max(7, min(10, rect.height * 0.13))))
|
|
2246
|
+
.foregroundColor(Palette.text.opacity(0.78))
|
|
2247
|
+
.lineLimit(1)
|
|
2248
|
+
if rect.width > 130, rect.height > 38, !win.title.isEmpty {
|
|
2249
|
+
Text(win.title)
|
|
2250
|
+
.font(Typo.mono(max(6, min(8, rect.height * 0.09))))
|
|
2251
|
+
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
2252
|
+
.lineLimit(1)
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
.padding(.horizontal, 5)
|
|
2256
|
+
.padding(.vertical, 3)
|
|
2257
|
+
.frame(maxWidth: rect.width, maxHeight: rect.height)
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
.allowsHitTesting(false)
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1983
2264
|
@ViewBuilder
|
|
1984
2265
|
private func windowTile(win: ScreenMapWindowEntry, editor: ScreenMapEditorState?, metrics: CanvasMetrics) -> some View {
|
|
1985
2266
|
let rect = metrics.mapRect(for: win.virtualFrame)
|
|
@@ -1990,91 +2271,93 @@ struct ScreenMapView: View {
|
|
|
1990
2271
|
let isInActiveLayer = editor?.isLayerSelected(win.layer) ?? true
|
|
1991
2272
|
let winLayerColor = Self.layerColor(for: win.layer)
|
|
1992
2273
|
let isSearchHighlighted = controller.searchHighlightedWindowId == win.id
|
|
2274
|
+
let usesFlatStyle = editor?.isShowingAll ?? true
|
|
1993
2275
|
|
|
1994
2276
|
let fillColor = isSearchHighlighted
|
|
1995
2277
|
? Self.shelfGreen.opacity(0.2)
|
|
1996
2278
|
: isSelected
|
|
1997
2279
|
? Palette.running.opacity(0.18)
|
|
1998
|
-
: win.hasEdits ? Color.orange.opacity(0.
|
|
2280
|
+
: win.hasEdits ? Color.orange.opacity(0.14) : usesFlatStyle ? Palette.surface.opacity(0.68) : winLayerColor.opacity(0.16)
|
|
1999
2281
|
let borderColor = isSearchHighlighted
|
|
2000
2282
|
? Self.shelfGreen.opacity(0.8)
|
|
2001
2283
|
: isSelected
|
|
2002
2284
|
? Palette.running.opacity(0.8)
|
|
2003
|
-
: win.hasEdits ? Color.orange.opacity(0.
|
|
2285
|
+
: win.hasEdits ? Color.orange.opacity(0.65) : usesFlatStyle ? Palette.border.opacity(0.55) : winLayerColor.opacity(0.42)
|
|
2004
2286
|
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
.fill(fillColor)
|
|
2014
|
-
.overlay(
|
|
2015
|
-
RoundedRectangle(cornerRadius: 2)
|
|
2016
|
-
.strokeBorder(borderColor, lineWidth: isSearchHighlighted ? 2 : isSelected ? 1.5 : 0.5)
|
|
2017
|
-
)
|
|
2018
|
-
.overlay(alignment: .leading) {
|
|
2287
|
+
RoundedRectangle(cornerRadius: 2)
|
|
2288
|
+
.fill(fillColor)
|
|
2289
|
+
.overlay(
|
|
2290
|
+
RoundedRectangle(cornerRadius: 2)
|
|
2291
|
+
.strokeBorder(borderColor, lineWidth: isSearchHighlighted ? 2 : isSelected ? 1.5 : 0.5)
|
|
2292
|
+
)
|
|
2293
|
+
.overlay(alignment: .leading) {
|
|
2294
|
+
if !usesFlatStyle {
|
|
2019
2295
|
Rectangle()
|
|
2020
2296
|
.fill(winLayerColor)
|
|
2021
2297
|
.frame(width: 2)
|
|
2022
2298
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2299
|
+
}
|
|
2300
|
+
.clipShape(RoundedRectangle(cornerRadius: 2))
|
|
2301
|
+
.overlay {
|
|
2302
|
+
ZStack {
|
|
2303
|
+
VStack(spacing: 1) {
|
|
2304
|
+
Text(win.app)
|
|
2305
|
+
.font(Typo.monoBold(max(7, min(10, h * 0.15))))
|
|
2306
|
+
.foregroundColor(isSelected ? Palette.running : Palette.text)
|
|
2307
|
+
.lineLimit(1)
|
|
2308
|
+
if h > 30 {
|
|
2309
|
+
Text(win.title)
|
|
2310
|
+
.font(Typo.mono(max(6, min(8, h * 0.1))))
|
|
2311
|
+
.foregroundColor(Palette.textDim)
|
|
2030
2312
|
.lineLimit(1)
|
|
2031
|
-
if h > 30 {
|
|
2032
|
-
Text(win.title)
|
|
2033
|
-
.font(Typo.mono(max(6, min(8, h * 0.1))))
|
|
2034
|
-
.foregroundColor(Palette.textDim)
|
|
2035
|
-
.lineLimit(1)
|
|
2036
|
-
}
|
|
2037
|
-
if h > 50 {
|
|
2038
|
-
Text("\(Int(win.virtualFrame.width))x\(Int(win.virtualFrame.height))")
|
|
2039
|
-
.font(Typo.mono(6))
|
|
2040
|
-
.foregroundColor(Palette.textMuted)
|
|
2041
|
-
}
|
|
2042
2313
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2314
|
+
if h > 50 {
|
|
2315
|
+
Text("\(Int(win.virtualFrame.width))x\(Int(win.virtualFrame.height))")
|
|
2316
|
+
.font(Typo.mono(6))
|
|
2317
|
+
.foregroundColor(Palette.textMuted)
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
.padding(.leading, 4)
|
|
2321
|
+
.padding(2)
|
|
2045
2322
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
Spacer()
|
|
2050
|
-
Image(systemName: tileIcon)
|
|
2051
|
-
.font(.system(size: 6))
|
|
2052
|
-
.foregroundColor(Color.white.opacity(0.3))
|
|
2053
|
-
.padding(2)
|
|
2054
|
-
}
|
|
2323
|
+
if h > 40, let tileIcon = Self.inferTileIcon(for: win, displays: editor?.displays ?? []) {
|
|
2324
|
+
VStack {
|
|
2325
|
+
HStack {
|
|
2055
2326
|
Spacer()
|
|
2327
|
+
Image(systemName: tileIcon)
|
|
2328
|
+
.font(.system(size: 6))
|
|
2329
|
+
.foregroundColor(Color.white.opacity(0.3))
|
|
2330
|
+
.padding(2)
|
|
2056
2331
|
}
|
|
2332
|
+
Spacer()
|
|
2057
2333
|
}
|
|
2334
|
+
}
|
|
2058
2335
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2336
|
+
if h > 50, let session = Self.extractLatticesSession(from: win.title) {
|
|
2337
|
+
VStack {
|
|
2338
|
+
Spacer()
|
|
2339
|
+
HStack {
|
|
2340
|
+
Text("[\(session)]")
|
|
2341
|
+
.font(Typo.mono(6))
|
|
2342
|
+
.foregroundColor(Palette.running.opacity(0.7))
|
|
2343
|
+
.lineLimit(1)
|
|
2344
|
+
.padding(.leading, 4)
|
|
2345
|
+
.padding(.bottom, 2)
|
|
2061
2346
|
Spacer()
|
|
2062
|
-
HStack {
|
|
2063
|
-
Text("[\(session)]")
|
|
2064
|
-
.font(Typo.mono(6))
|
|
2065
|
-
.foregroundColor(Palette.running.opacity(0.7))
|
|
2066
|
-
.lineLimit(1)
|
|
2067
|
-
.padding(.leading, 4)
|
|
2068
|
-
.padding(.bottom, 2)
|
|
2069
|
-
Spacer()
|
|
2070
|
-
}
|
|
2071
2347
|
}
|
|
2072
2348
|
}
|
|
2073
2349
|
}
|
|
2074
2350
|
}
|
|
2075
|
-
|
|
2076
|
-
.buttonStyle(.plain)
|
|
2351
|
+
}
|
|
2077
2352
|
.frame(width: w, height: h)
|
|
2353
|
+
.contentShape(Rectangle())
|
|
2354
|
+
.onTapGesture {
|
|
2355
|
+
if NSEvent.modifierFlags.contains(.command) {
|
|
2356
|
+
controller.toggleSelection(win.id)
|
|
2357
|
+
} else {
|
|
2358
|
+
controller.selectSingle(win.id)
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2078
2361
|
.overlay {
|
|
2079
2362
|
if isSelected && w > 30 && h > 20 {
|
|
2080
2363
|
resizeHandles(width: w, height: h)
|
|
@@ -2191,6 +2474,7 @@ struct ScreenMapView: View {
|
|
|
2191
2474
|
Rectangle().fill(Palette.border).frame(width: 0.5, height: 12)
|
|
2192
2475
|
|
|
2193
2476
|
Button {
|
|
2477
|
+
controller.clearSelection()
|
|
2194
2478
|
controller.focusViewportPreset(.overview)
|
|
2195
2479
|
} label: {
|
|
2196
2480
|
Text("\(pct)%")
|
|
@@ -2232,15 +2516,12 @@ struct ScreenMapView: View {
|
|
|
2232
2516
|
Rectangle().fill(Color.white.opacity(0.04)).frame(height: 0.5)
|
|
2233
2517
|
HStack(spacing: 6) {
|
|
2234
2518
|
if let editor = controller.editor {
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
: Palette.running
|
|
2238
|
-
Circle().fill(layerColor).frame(width: 5, height: 5)
|
|
2239
|
-
Text(editor.layerLabel)
|
|
2519
|
+
Circle().fill(Palette.running).frame(width: 5, height: 5)
|
|
2520
|
+
Text("DESKTOP")
|
|
2240
2521
|
.font(Typo.monoBold(8))
|
|
2241
|
-
.foregroundColor(
|
|
2522
|
+
.foregroundColor(Palette.running)
|
|
2242
2523
|
Text("·").foregroundColor(Palette.textMuted).font(Typo.mono(7))
|
|
2243
|
-
Text("\(editor.
|
|
2524
|
+
Text("\(editor.renderedCanvasWindows.count) windows")
|
|
2244
2525
|
.font(Typo.mono(8))
|
|
2245
2526
|
.foregroundColor(Palette.textDim)
|
|
2246
2527
|
if let focused = editor.focusedDisplay {
|
|
@@ -2493,7 +2774,8 @@ struct ScreenMapView: View {
|
|
|
2493
2774
|
|
|
2494
2775
|
HStack(spacing: 6) {
|
|
2495
2776
|
mapScopePill("ALL", isActive: editor.focusedDisplayIndex == nil) {
|
|
2496
|
-
controller.
|
|
2777
|
+
controller.clearSelection()
|
|
2778
|
+
controller.focusViewportPreset(.overview)
|
|
2497
2779
|
}
|
|
2498
2780
|
ForEach(editor.spatialDisplayOrder, id: \.index) { disp in
|
|
2499
2781
|
mapScopePill("\(editor.spatialNumber(for: disp.index))", isActive: editor.focusedDisplayIndex == disp.index) {
|
|
@@ -2640,6 +2922,16 @@ struct ScreenMapView: View {
|
|
|
2640
2922
|
}
|
|
2641
2923
|
|
|
2642
2924
|
private func syncCanvasGeometry(editor: ScreenMapEditorState?, metrics: CanvasMetrics) {
|
|
2925
|
+
let previous = editor.map {
|
|
2926
|
+
(
|
|
2927
|
+
fitScale: $0.fitScale,
|
|
2928
|
+
scale: $0.scale,
|
|
2929
|
+
mapOrigin: $0.mapOrigin,
|
|
2930
|
+
viewportSize: $0.viewportSize,
|
|
2931
|
+
screenSize: $0.screenSize,
|
|
2932
|
+
bboxOrigin: $0.bboxOrigin
|
|
2933
|
+
)
|
|
2934
|
+
}
|
|
2643
2935
|
editor?.fitScale = metrics.fitScale
|
|
2644
2936
|
editor?.scale = metrics.effectiveScale
|
|
2645
2937
|
editor?.mapOrigin = metrics.centerOffset
|
|
@@ -2647,6 +2939,55 @@ struct ScreenMapView: View {
|
|
|
2647
2939
|
editor?.screenSize = metrics.worldBounds.size
|
|
2648
2940
|
editor?.bboxOrigin = metrics.worldBounds.origin
|
|
2649
2941
|
controller.applyPendingCanvasNavigationIfNeeded()
|
|
2942
|
+
if let editor {
|
|
2943
|
+
let boundedPan = boundedPanOffset(editor.panOffset, editor: editor)
|
|
2944
|
+
if boundedPan != editor.panOffset {
|
|
2945
|
+
editor.panOffset = boundedPan
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
if let editor,
|
|
2949
|
+
let previous,
|
|
2950
|
+
previous.fitScale != editor.fitScale ||
|
|
2951
|
+
previous.scale != editor.scale ||
|
|
2952
|
+
previous.mapOrigin != editor.mapOrigin ||
|
|
2953
|
+
previous.viewportSize != editor.viewportSize ||
|
|
2954
|
+
previous.screenSize != editor.screenSize ||
|
|
2955
|
+
previous.bboxOrigin != editor.bboxOrigin {
|
|
2956
|
+
DispatchQueue.main.async {
|
|
2957
|
+
editor.objectWillChange.send()
|
|
2958
|
+
controller.objectWillChange.send()
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
private func boundedPanOffset(_ proposed: CGPoint, editor: ScreenMapEditorState) -> CGPoint {
|
|
2964
|
+
guard editor.scale > 0,
|
|
2965
|
+
editor.viewportSize.width > 0,
|
|
2966
|
+
editor.viewportSize.height > 0,
|
|
2967
|
+
editor.screenSize.width > 0,
|
|
2968
|
+
editor.screenSize.height > 0 else {
|
|
2969
|
+
return proposed
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
func clampAxis(_ value: CGFloat, mapOrigin: CGFloat, content: CGFloat, viewport: CGFloat) -> CGFloat {
|
|
2973
|
+
let effectiveScale = max(editor.fitScale * editor.zoomLevel, editor.scale)
|
|
2974
|
+
let mapSize = content * effectiveScale
|
|
2975
|
+
let minVisible = min(Self.canvasPanMinVisiblePixels, max(viewport / 2, 0))
|
|
2976
|
+
let minValue = minVisible - mapOrigin - mapSize
|
|
2977
|
+
let maxValue = viewport - minVisible - mapOrigin
|
|
2978
|
+
return min(max(value, minValue), maxValue)
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
return CGPoint(
|
|
2982
|
+
x: clampAxis(proposed.x,
|
|
2983
|
+
mapOrigin: editor.mapOrigin.x,
|
|
2984
|
+
content: editor.screenSize.width,
|
|
2985
|
+
viewport: editor.viewportSize.width),
|
|
2986
|
+
y: clampAxis(proposed.y,
|
|
2987
|
+
mapOrigin: editor.mapOrigin.y,
|
|
2988
|
+
content: editor.screenSize.height,
|
|
2989
|
+
viewport: editor.viewportSize.height)
|
|
2990
|
+
)
|
|
2650
2991
|
}
|
|
2651
2992
|
|
|
2652
2993
|
// MARK: - Layer Colors
|
|
@@ -2754,7 +3095,7 @@ struct ScreenMapView: View {
|
|
|
2754
3095
|
} else if event.type == .keyUp {
|
|
2755
3096
|
guard isSpaceHeld else { return nil }
|
|
2756
3097
|
isSpaceHeld = false
|
|
2757
|
-
|
|
3098
|
+
canvasPanStart = nil
|
|
2758
3099
|
NSCursor.pop()
|
|
2759
3100
|
return nil
|
|
2760
3101
|
}
|
|
@@ -2795,36 +3136,54 @@ struct ScreenMapView: View {
|
|
|
2795
3136
|
guard let eventWindow = event.window,
|
|
2796
3137
|
eventWindow === ScreenMapWindowController.shared.nsWindow else { return event }
|
|
2797
3138
|
let flippedPt = flippedScreenPoint(event)
|
|
3139
|
+
guard isCanvasInteractionEvent(event, flippedPoint: flippedPt) else { return event }
|
|
2798
3140
|
|
|
2799
3141
|
// Space+click → begin canvas pan
|
|
2800
3142
|
if isSpaceHeld,
|
|
2801
|
-
isCanvasPoint(flippedPt),
|
|
2802
3143
|
let editor = controller.editor {
|
|
2803
|
-
|
|
2804
|
-
|
|
3144
|
+
canvasPanStart = event.locationInWindow
|
|
3145
|
+
canvasPanStartOffset = editor.panOffset
|
|
2805
3146
|
NSCursor.closedHand.push()
|
|
2806
3147
|
return nil
|
|
2807
3148
|
}
|
|
2808
3149
|
|
|
2809
3150
|
if let editor = controller.editor,
|
|
2810
|
-
let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor)
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3151
|
+
let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor) {
|
|
3152
|
+
screenMapClickWindowId = nil
|
|
3153
|
+
if NSEvent.modifierFlags.contains(.command) {
|
|
3154
|
+
controller.toggleSelection(hit.id)
|
|
3155
|
+
} else if !controller.isSelected(hit.id) {
|
|
3156
|
+
controller.selectSingle(hit.id)
|
|
3157
|
+
}
|
|
3158
|
+
canvasPanStart = event.locationInWindow
|
|
3159
|
+
canvasPanStartOffset = editor.panOffset
|
|
3160
|
+
return nil
|
|
2815
3161
|
} else {
|
|
2816
3162
|
screenMapClickWindowId = nil
|
|
2817
3163
|
}
|
|
3164
|
+
|
|
3165
|
+
if let editor = controller.editor {
|
|
3166
|
+
canvasPanStart = event.locationInWindow
|
|
3167
|
+
canvasPanStartOffset = editor.panOffset
|
|
3168
|
+
return nil
|
|
3169
|
+
}
|
|
3170
|
+
|
|
2818
3171
|
return event
|
|
2819
3172
|
}
|
|
2820
3173
|
|
|
2821
3174
|
mouseDragMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDragged) { event in
|
|
2822
|
-
// Space+drag
|
|
2823
|
-
if
|
|
3175
|
+
// Empty-canvas drag, or Space+drag, pans the viewport.
|
|
3176
|
+
if let start = canvasPanStart, let editor = controller.editor {
|
|
2824
3177
|
let dx = event.locationInWindow.x - start.x
|
|
2825
3178
|
let dy = event.locationInWindow.y - start.y
|
|
2826
|
-
|
|
2827
|
-
|
|
3179
|
+
let bounded = boundedPanOffset(
|
|
3180
|
+
CGPoint(x: canvasPanStartOffset.x + dx, y: canvasPanStartOffset.y - dy),
|
|
3181
|
+
editor: editor
|
|
3182
|
+
)
|
|
3183
|
+
if bounded != editor.panOffset {
|
|
3184
|
+
editor.activeViewportPreset = nil
|
|
3185
|
+
editor.panOffset = bounded
|
|
3186
|
+
}
|
|
2828
3187
|
return nil
|
|
2829
3188
|
}
|
|
2830
3189
|
|
|
@@ -2899,10 +3258,12 @@ struct ScreenMapView: View {
|
|
|
2899
3258
|
}
|
|
2900
3259
|
|
|
2901
3260
|
mouseUpMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseUp) { event in
|
|
2902
|
-
// End
|
|
2903
|
-
if
|
|
2904
|
-
|
|
2905
|
-
|
|
3261
|
+
// End canvas pan.
|
|
3262
|
+
if canvasPanStart != nil {
|
|
3263
|
+
canvasPanStart = nil
|
|
3264
|
+
if isSpaceHeld {
|
|
3265
|
+
NSCursor.pop() // pop closedHand, openHand remains
|
|
3266
|
+
}
|
|
2906
3267
|
return event
|
|
2907
3268
|
}
|
|
2908
3269
|
if screenMapClickWindowId != nil {
|
|
@@ -2922,8 +3283,7 @@ struct ScreenMapView: View {
|
|
|
2922
3283
|
let editor = controller.editor else { return event }
|
|
2923
3284
|
|
|
2924
3285
|
let flippedPt = flippedScreenPoint(event)
|
|
2925
|
-
|
|
2926
|
-
guard canvasRect.contains(flippedPt) else { return event }
|
|
3286
|
+
guard isCanvasInteractionEvent(event, flippedPoint: flippedPt) else { return event }
|
|
2927
3287
|
|
|
2928
3288
|
if let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor) {
|
|
2929
3289
|
if !controller.isSelected(hit.id) {
|
|
@@ -2952,10 +3312,9 @@ struct ScreenMapView: View {
|
|
|
2952
3312
|
}
|
|
2953
3313
|
|
|
2954
3314
|
let flippedPt = flippedScreenPoint(event)
|
|
2955
|
-
|
|
2956
|
-
guard canvasRect.contains(flippedPt) else { return event }
|
|
3315
|
+
guard isCanvasInteractionEvent(event, flippedPoint: flippedPt) else { return event }
|
|
2957
3316
|
|
|
2958
|
-
let isZoom = event.modifierFlags.contains(.command)
|
|
3317
|
+
let isZoom = event.modifierFlags.contains(.command)
|
|
2959
3318
|
|
|
2960
3319
|
if isZoom {
|
|
2961
3320
|
let zoomDelta: CGFloat = event.hasPreciseScrollingDeltas ? event.scrollingDeltaY * 0.01 : event.scrollingDeltaY * 0.05
|
|
@@ -2980,13 +3339,30 @@ struct ScreenMapView: View {
|
|
|
2980
3339
|
|
|
2981
3340
|
editor.activeViewportPreset = nil
|
|
2982
3341
|
editor.zoomLevel = newZoom
|
|
2983
|
-
editor.
|
|
3342
|
+
editor.scale = editor.fitScale * newZoom
|
|
3343
|
+
let bounded = boundedPanOffset(CGPoint(x: newPanX, y: newPanY), editor: editor)
|
|
3344
|
+
if bounded != editor.panOffset {
|
|
3345
|
+
editor.activeViewportPreset = nil
|
|
3346
|
+
editor.panOffset = bounded
|
|
3347
|
+
}
|
|
2984
3348
|
} else {
|
|
2985
|
-
editor.
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3349
|
+
guard editor.zoomLevel > 1.0001 else {
|
|
3350
|
+
if editor.panOffset != .zero {
|
|
3351
|
+
editor.panOffset = .zero
|
|
3352
|
+
}
|
|
3353
|
+
return nil
|
|
3354
|
+
}
|
|
3355
|
+
let bounded = boundedPanOffset(
|
|
3356
|
+
CGPoint(
|
|
3357
|
+
x: editor.panOffset.x + event.scrollingDeltaX,
|
|
3358
|
+
y: editor.panOffset.y - event.scrollingDeltaY
|
|
3359
|
+
),
|
|
3360
|
+
editor: editor
|
|
2989
3361
|
)
|
|
3362
|
+
if bounded != editor.panOffset {
|
|
3363
|
+
editor.activeViewportPreset = nil
|
|
3364
|
+
editor.panOffset = bounded
|
|
3365
|
+
}
|
|
2990
3366
|
}
|
|
2991
3367
|
return nil
|
|
2992
3368
|
}
|
|
@@ -3000,8 +3376,7 @@ struct ScreenMapView: View {
|
|
|
3000
3376
|
}
|
|
3001
3377
|
|
|
3002
3378
|
let flippedPt = flippedScreenPoint(event)
|
|
3003
|
-
|
|
3004
|
-
guard canvasRect.contains(flippedPt) else {
|
|
3379
|
+
guard isCanvasInteractionEvent(event, flippedPoint: flippedPt) else {
|
|
3005
3380
|
resetCursorIfNeeded()
|
|
3006
3381
|
return event
|
|
3007
3382
|
}
|
|
@@ -3072,6 +3447,25 @@ struct ScreenMapView: View {
|
|
|
3072
3447
|
CGRect(origin: screenMapCanvasOrigin, size: screenMapCanvasSize).contains(point)
|
|
3073
3448
|
}
|
|
3074
3449
|
|
|
3450
|
+
private func isCanvasInteractionEvent(_ event: NSEvent, flippedPoint: CGPoint) -> Bool {
|
|
3451
|
+
guard isCanvasPoint(flippedPoint),
|
|
3452
|
+
let contentWidth = event.window?.contentView?.bounds.width else {
|
|
3453
|
+
return false
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
let x = event.locationInWindow.x
|
|
3457
|
+
let leftBoundary = sidebarWidth + 8
|
|
3458
|
+
let rightBoundary = max(leftBoundary, contentWidth - inspectorWidth - 8)
|
|
3459
|
+
return x >= leftBoundary && x <= rightBoundary
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
private func flippedScreenPoint(_ event: NSEvent) -> CGPoint {
|
|
3463
|
+
guard let nsWindow = event.window else { return .zero }
|
|
3464
|
+
let loc = event.locationInWindow
|
|
3465
|
+
let windowHeight = nsWindow.contentView?.frame.height ?? nsWindow.frame.height
|
|
3466
|
+
return CGPoint(x: loc.x, y: windowHeight - loc.y)
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3075
3469
|
private func detectDragMode(mapPoint: CGPoint, windowMapRect: CGRect) -> CanvasDragMode {
|
|
3076
3470
|
let w = windowMapRect.width
|
|
3077
3471
|
let h = windowMapRect.height
|
|
@@ -3097,13 +3491,6 @@ struct ScreenMapView: View {
|
|
|
3097
3491
|
return .move
|
|
3098
3492
|
}
|
|
3099
3493
|
|
|
3100
|
-
private func flippedScreenPoint(_ event: NSEvent) -> CGPoint {
|
|
3101
|
-
guard let nsWindow = event.window else { return .zero }
|
|
3102
|
-
let loc = event.locationInWindow
|
|
3103
|
-
let windowHeight = nsWindow.contentView?.frame.height ?? nsWindow.frame.height
|
|
3104
|
-
return CGPoint(x: loc.x, y: windowHeight - loc.y)
|
|
3105
|
-
}
|
|
3106
|
-
|
|
3107
3494
|
// MARK: - Context Menu
|
|
3108
3495
|
|
|
3109
3496
|
private func showLayerContextMenu(for windowId: UInt32, at point: NSPoint, in window: NSWindow, editor: ScreenMapEditorState) {
|
|
@@ -3549,6 +3936,37 @@ struct ShowOnScreenBezelView: View {
|
|
|
3549
3936
|
}
|
|
3550
3937
|
}
|
|
3551
3938
|
|
|
3939
|
+
private struct SidebarWindowHitCatcher: NSViewRepresentable {
|
|
3940
|
+
let rowHeight: CGFloat
|
|
3941
|
+
let onClick: (Int) -> Void
|
|
3942
|
+
|
|
3943
|
+
func makeNSView(context: Context) -> HitView {
|
|
3944
|
+
let view = HitView()
|
|
3945
|
+
view.rowHeight = rowHeight
|
|
3946
|
+
view.onClick = onClick
|
|
3947
|
+
return view
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
func updateNSView(_ nsView: HitView, context: Context) {
|
|
3951
|
+
nsView.rowHeight = rowHeight
|
|
3952
|
+
nsView.onClick = onClick
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
final class HitView: NSView {
|
|
3956
|
+
var rowHeight: CGFloat = 30
|
|
3957
|
+
var onClick: ((Int) -> Void)?
|
|
3958
|
+
|
|
3959
|
+
override var isFlipped: Bool { true }
|
|
3960
|
+
|
|
3961
|
+
override func mouseDown(with event: NSEvent) {
|
|
3962
|
+
let point = convert(event.locationInWindow, from: nil)
|
|
3963
|
+
guard point.x >= 0, point.x <= bounds.width, point.y >= 0, point.y <= bounds.height else { return }
|
|
3964
|
+
let row = max(0, Int(point.y / max(rowHeight, 1)))
|
|
3965
|
+
onClick?(row)
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3552
3970
|
// MARK: - Preference Keys
|
|
3553
3971
|
|
|
3554
3972
|
private struct SearchOverlayFrameKey: PreferenceKey {
|