Mission Control Turbo: macOS multitasking turbocharged
at main 164 lines 6.2 kB view raw
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}