Mission Control Turbo: macOS multitasking turbocharged
1import CoreGraphics
2import Foundation
3
4/// A positioned item in the overlay layout.
5struct LayoutItem: Identifiable {
6 let windowInfo: WindowInfo
7 let frame: CGRect // absolute position in screen coords
8 let groupIndex: Int
9
10 var id: CGWindowID { windowInfo.windowID }
11}
12
13/// A positioned group header.
14struct LayoutGroupHeader: Identifiable {
15 let group: WindowGroup
16 let frame: CGRect
17
18 var id: String { group.id }
19}
20
21/// Per-group layout metadata for keyboard navigation.
22struct GroupLayout {
23 let columns: Int
24 let thumbnailSize: CGSize
25 let itemRange: Range<Int> // range of indices into LayoutResult.items
26}
27
28/// Complete layout result for one overlay activation.
29struct LayoutResult {
30 let items: [LayoutItem]
31 let headers: [LayoutGroupHeader]
32 let groupLayouts: [GroupLayout]
33 let totalHeight: CGFloat
34 let isCompactHeaders: Bool
35
36 var thumbnailSize: CGSize { groupLayouts.first?.thumbnailSize ?? CGSize(width: 200, height: 150) }
37 var columns: Int { groupLayouts.first?.columns ?? 1 }
38}
39
40/// Core layout engine: flat grid placement maximizing thumbnail size.
41final class LayoutEngine {
42 private let layoutState = LayoutState()
43
44 /// Compute layout for a snapshot on the given screen bounds.
45 /// Uses a flat grid — all windows in one grid, sorted by app, no per-app rows.
46 func layout(snapshot: WindowSnapshot, screenBounds: CGRect) -> LayoutResult {
47 let availableWidth = screenBounds.width - LayoutGeometry.margin * 2
48 let availableHeight = screenBounds.height - LayoutGeometry.margin * 2
49
50 // Flatten all windows in group order (already sorted by app name)
51 let allWindows = snapshot.groups.flatMap { $0.windows }
52
53 // Check stability against previous layout
54 let shouldReuse: Bool = {
55 let previousKeys = layoutState.lastFingerprint
56 guard !previousKeys.isEmpty else { return false }
57 let currentKeys = snapshot.fingerprint
58 let intersection = currentKeys.intersection(previousKeys)
59 let union = currentKeys.union(previousKeys)
60 guard !union.isEmpty else { return false }
61 return Double(intersection.count) / Double(union.count) > 0.7
62 }()
63
64 // Compute optimal grid
65 let (thumbWidth, cols) = LayoutGeometry.optimalGrid(
66 windows: allWindows,
67 availableWidth: availableWidth,
68 availableHeight: availableHeight
69 )
70
71 // Place all items in a flat grid
72 var items: [LayoutItem] = []
73 var y = screenBounds.origin.y + LayoutGeometry.margin
74
75 let rowCount = Int(ceil(Double(allWindows.count) / Double(cols)))
76 for row in 0..<rowCount {
77 let rowStart = row * cols
78 let rowEnd = min(rowStart + cols, allWindows.count)
79
80 // Compute row height from tallest cell
81 var maxRowH: CGFloat = 0
82 for idx in rowStart..<rowEnd {
83 let w = allWindows[idx]
84 let ratio = w.frame.width > 0 ? w.frame.height / w.frame.width : 0.625
85 let cellH = thumbWidth * ratio + LayoutGeometry.titleHeight
86 maxRowH = max(maxRowH, cellH)
87 }
88
89 // Center this row's items horizontally
90 let windowsInRow = rowEnd - rowStart
91 let gridWidth = CGFloat(windowsInRow) * thumbWidth + CGFloat(max(0, windowsInRow - 1)) * LayoutGeometry.itemSpacing
92 let xOffset = screenBounds.origin.x + LayoutGeometry.margin + (availableWidth - gridWidth) / 2
93
94 for idx in rowStart..<rowEnd {
95 let window = allWindows[idx]
96 let col = idx - rowStart
97 let ratio = window.frame.width > 0 ? window.frame.height / window.frame.width : 0.625
98 let cellH = thumbWidth * ratio + LayoutGeometry.titleHeight
99
100 let x = xOffset + CGFloat(col) * (thumbWidth + LayoutGeometry.itemSpacing)
101 let cellY = y + (maxRowH - cellH) / 2
102
103 // Find group index for this window
104 let groupIndex = snapshot.groups.firstIndex(where: { $0.appBundleID == window.appBundleID }) ?? 0
105
106 var frame = CGRect(x: x, y: cellY, width: thumbWidth, height: cellH)
107
108 if shouldReuse, let stored = layoutState.position(for: window.fingerprintKey) {
109 frame = stored.absoluteRect(in: screenBounds)
110 }
111
112 items.append(LayoutItem(windowInfo: window, frame: frame, groupIndex: groupIndex))
113 }
114
115 y += maxRowH
116 if row < rowCount - 1 { y += LayoutGeometry.itemSpacing }
117 }
118
119 let totalHeight = y - screenBounds.origin.y
120
121 // Store positions for stability
122 for item in items {
123 layoutState.store(
124 key: item.windowInfo.fingerprintKey,
125 position: LayoutPosition.from(rect: item.frame, in: screenBounds)
126 )
127 }
128 layoutState.updateFingerprint(snapshot.fingerprint)
129 layoutState.prune(keeping: snapshot.fingerprint)
130
131 // Center vertically if there's excess space
132 let excessHeight = screenBounds.height - totalHeight
133 if excessHeight > 0 {
134 let offset = excessHeight / 2
135 items = items.map { item in
136 LayoutItem(
137 windowInfo: item.windowInfo,
138 frame: item.frame.offsetBy(dx: 0, dy: offset),
139 groupIndex: item.groupIndex
140 )
141 }
142 }
143
144 // Single group layout for keyboard navigation (flat grid)
145 let groupLayouts = [GroupLayout(
146 columns: cols,
147 thumbnailSize: CGSize(width: thumbWidth, height: thumbWidth * 0.625 + LayoutGeometry.titleHeight),
148 itemRange: 0..<items.count
149 )]
150
151 return LayoutResult(
152 items: items,
153 headers: [],
154 groupLayouts: groupLayouts,
155 totalHeight: totalHeight,
156 isCompactHeaders: true // always show app badge on each thumbnail
157 )
158 }
159
160 /// Force a full relayout on next activation.
161 func invalidate() {
162 layoutState.clear()
163 }
164}