import CoreGraphics import Foundation /// A positioned item in the overlay layout. struct LayoutItem: Identifiable { let windowInfo: WindowInfo let frame: CGRect // absolute position in screen coords let groupIndex: Int var id: CGWindowID { windowInfo.windowID } } /// A positioned group header. struct LayoutGroupHeader: Identifiable { let group: WindowGroup let frame: CGRect var id: String { group.id } } /// Per-group layout metadata for keyboard navigation. struct GroupLayout { let columns: Int let thumbnailSize: CGSize let itemRange: Range // range of indices into LayoutResult.items } /// Complete layout result for one overlay activation. struct LayoutResult { let items: [LayoutItem] let headers: [LayoutGroupHeader] let groupLayouts: [GroupLayout] let totalHeight: CGFloat let isCompactHeaders: Bool var thumbnailSize: CGSize { groupLayouts.first?.thumbnailSize ?? CGSize(width: 200, height: 150) } var columns: Int { groupLayouts.first?.columns ?? 1 } } /// Core layout engine: flat grid placement maximizing thumbnail size. final class LayoutEngine { private let layoutState = LayoutState() /// Compute layout for a snapshot on the given screen bounds. /// Uses a flat grid — all windows in one grid, sorted by app, no per-app rows. func layout(snapshot: WindowSnapshot, screenBounds: CGRect) -> LayoutResult { let availableWidth = screenBounds.width - LayoutGeometry.margin * 2 let availableHeight = screenBounds.height - LayoutGeometry.margin * 2 // Flatten all windows in group order (already sorted by app name) let allWindows = snapshot.groups.flatMap { $0.windows } // Check stability against previous layout let shouldReuse: Bool = { let previousKeys = layoutState.lastFingerprint guard !previousKeys.isEmpty else { return false } let currentKeys = snapshot.fingerprint let intersection = currentKeys.intersection(previousKeys) let union = currentKeys.union(previousKeys) guard !union.isEmpty else { return false } return Double(intersection.count) / Double(union.count) > 0.7 }() // Compute optimal grid let (thumbWidth, cols) = LayoutGeometry.optimalGrid( windows: allWindows, availableWidth: availableWidth, availableHeight: availableHeight ) // Place all items in a flat grid var items: [LayoutItem] = [] var y = screenBounds.origin.y + LayoutGeometry.margin let rowCount = Int(ceil(Double(allWindows.count) / Double(cols))) for row in 0.. 0 ? w.frame.height / w.frame.width : 0.625 let cellH = thumbWidth * ratio + LayoutGeometry.titleHeight maxRowH = max(maxRowH, cellH) } // Center this row's items horizontally let windowsInRow = rowEnd - rowStart let gridWidth = CGFloat(windowsInRow) * thumbWidth + CGFloat(max(0, windowsInRow - 1)) * LayoutGeometry.itemSpacing let xOffset = screenBounds.origin.x + LayoutGeometry.margin + (availableWidth - gridWidth) / 2 for idx in rowStart.. 0 ? window.frame.height / window.frame.width : 0.625 let cellH = thumbWidth * ratio + LayoutGeometry.titleHeight let x = xOffset + CGFloat(col) * (thumbWidth + LayoutGeometry.itemSpacing) let cellY = y + (maxRowH - cellH) / 2 // Find group index for this window let groupIndex = snapshot.groups.firstIndex(where: { $0.appBundleID == window.appBundleID }) ?? 0 var frame = CGRect(x: x, y: cellY, width: thumbWidth, height: cellH) if shouldReuse, let stored = layoutState.position(for: window.fingerprintKey) { frame = stored.absoluteRect(in: screenBounds) } items.append(LayoutItem(windowInfo: window, frame: frame, groupIndex: groupIndex)) } y += maxRowH if row < rowCount - 1 { y += LayoutGeometry.itemSpacing } } let totalHeight = y - screenBounds.origin.y // Store positions for stability for item in items { layoutState.store( key: item.windowInfo.fingerprintKey, position: LayoutPosition.from(rect: item.frame, in: screenBounds) ) } layoutState.updateFingerprint(snapshot.fingerprint) layoutState.prune(keeping: snapshot.fingerprint) // Center vertically if there's excess space let excessHeight = screenBounds.height - totalHeight if excessHeight > 0 { let offset = excessHeight / 2 items = items.map { item in LayoutItem( windowInfo: item.windowInfo, frame: item.frame.offsetBy(dx: 0, dy: offset), groupIndex: item.groupIndex ) } } // Single group layout for keyboard navigation (flat grid) let groupLayouts = [GroupLayout( columns: cols, thumbnailSize: CGSize(width: thumbWidth, height: thumbWidth * 0.625 + LayoutGeometry.titleHeight), itemRange: 0..